Perl Weekly Challenge 183.

My solutions (task 1 and task 2 ) to the The Weekly Challenge - 183.

Task 1: Unique Array

Submitted by: Mohammad S Anwar
You are given list of arrayrefs.

Write a script to remove the duplicate arrayrefs from the given list.

Example 1
Input: @list = ([1,2], [3,4], [5,6], [1,2])
Output: ([1,2], [3,4], [5,6])
Example 2
Input: @list = ([9,1], [3,7], [2,5], [2,5])
Output: ([9, 1], [3,7], [2,5])

I use the magic of the Perl Data Language PDL to solve this task. I construct the solution progressively below. First I input the array of vectors as an 2d-array, one row for each vector, using PDL’s ability to parse strings.

perl -MPDL -MPDL::NiceSlice -E '
$x=pdl shift; say $x;
' "[[1,2], [3,4], [5,6], [1,2]]"

Results:

[
 [1 2]
 [3 4]
 [5 6]
 [1 2]
]

I compare each row with each other row by introducing dummy indices in the second and third positions and using andover to check if all elements of the vectors coincide.

perl -MPDL -MPDL::NiceSlice -E '
$x=pdl shift; $a=($x->(:,:,*1)==$x(:,*1,:))->andover; say $a;
' "[[1,2], [3,4], [5,6], [1,2]]"

The result is a 2D matrix

[
 [1 0 0 1]
 [0 1 0 0]
 [0 0 1 0]
 [1 0 0 1]
]

that shows that in this example, row 3 is equal to row 0. Besides, each row is equal to itself.

I eliminate the trivial case and the redundant cases by comparing the xvals and yvals of the matrix,

perl -MPDL -MPDL::NiceSlice -E '
$x=pdl shift; $a=($x->(:,:,*1)==$x(:,*1,:))->andover;
$w=$a&($a->xvals<$a->yvals); say +$w;
' "[[1,2], [3,4], [5,6], [1,2]]"

and orover the results

[
 [0 0 0 0]
 [0 0 0 0]
 [0 0 0 0]
 [1 0 0 0]
]

to get those rows that are duplicate of any other row:

perl -MPDL -MPDL::NiceSlice -E '
$x=pdl shift; $a=($x->(:,:,*1)==$x(:,*1,:))->andover;
$w=($a&($a->xvals<$a->yvals))->orover; say +$w;
' "[[1,2], [3,4], [5,6], [1,2]]"

The results

[0 0 0 1]

show that I must remove row-3. I invert the result find the rows that I have to keep and get their indices:

perl -MPDL -MPDL::NiceSlice -E '
$x=pdl shift; $a=($x->(:,:,*1)==$x(:,*1,:))->andover;
$w=which(!($a&($a->xvals<$a->yvals))->orover); say +$w;
' "[[1,2], [3,4], [5,6], [1,2]]"

which in this example are rows 0, 1 and 2. Results:

[0 1 2]

Finally, I use this as a vector of indices into the original array and print the results:

perl -MPDL -MPDL::NiceSlice -E '
$x=pdl shift; $a=($x->(:,:,*1)==$x(:,*1,:))->andover;
$w=which(!($a&($a->xvals<$a->yvals))->orover); say +$x->mv(-1,0)->index1d($w)->mv(0,-1);
' "[[1,2], [3,4], [5,6], [1,2]]"

Results:

[
 [1 2]
 [3 4]
 [5 6]
]

I test the code with the second example

perl -MPDL -MPDL::NiceSlice -E '
$x=pdl shift; $a=($x->(:,:,*1)==$x(:,*1,:))->andover;
$w=which(!($a&($a->xvals<$a->yvals))->orover); say +$x->mv(-1,0)->index1d($w)->mv(0,-1);
' "[[9,1], [3,7], [2,5], [2,5]]"

Results:

[
 [9 1]
 [3 7]
 [2 5]
]

With a very small modification I can make the code to work with ndarrays of any dimension as input, by flattening them before doing the comparisons with clump. Thus the complete code is

perl -MPDL -MPDL::NiceSlice -E '
$x=pdl shift; $f=$x->clump(-2); $a=($f->(:,:,*1)==$f(:,*1,:))->andover;
$w=which(!($a&($a->xvals<$a->yvals))->orover); say +$x->mv(-1,0)->index1d($w)->mv(0,-1);
' "[[1,2], [3,4], [5,6], [1,2]]"

Results:

[
 [1 2]
 [3 4]
 [5 6]
]

Thus, the original example still works.

Example with matricial, instead of vectorial inputs

perl -MPDL -MPDL::NiceSlice -E '
$x=pdl shift; $f=$x->clump(-2); $a=($f->(:,:,*1)==$f(:,*1,:))->andover;
$w=which(!($a&($a->xvals<$a->yvals))->orover); say +$x->mv(-1,0)->index1d($w)->mv(0,-1);
' "[[[1,2],[3,4]],[[5,6],[7,8]], [[5,6],[7,8]], [[9,10],[11,12]]]"

Results:

[
 [
  [1 2]
  [3 4]
 ]
 [
  [5 6]
  [7 8]
 ]
 [
  [ 9 10]
  [11 12]
 ]
]

Example with scalar instead of vectorial inputs:

perl -MPDL -MPDL::NiceSlice -E '
$x=pdl shift; $f=$x->clump(-2); $a=($f->(:,:,*1)==$f(:,*1,:))->andover;
$w=which(!($a&($a->xvals<$a->yvals))->orover); say +$x->mv(-1,0)->index1d($w)->mv(0,-1);
' "[0,1,0,2,3,0,2]"

Results:

[0 1 2 3]

The full code is

 1  # Perl weekly challenge 183
 2  # Task 1:  Unique Array
 3  #
 4  # See https://wlmb.github.io/2022/09/20/PWC183/#task-1-unique-array
 5  use v5.36;
 6  use experimental qw(try);
 7  use PDL;
 8  use PDL::NiceSlice;
 9  die <<EOF
10  Usage: $0 "[V1,V2...]" ...
11  to print the unique arrays among V1, V2...
12  Each V_i is an array [N1, N2...] of numbers N_j, or an array of arrays [W1,W2...] where
13  each Wj is an array of...
14  Admits multiple arguments.
15  EOF
16  unless @ARGV;
17  for(@ARGV){
18      try {
19          my $input=pdl $_;
20          my $flat=$input->clump(-2);
21          my $repetitions=($flat->(:,:,*1)==$flat(:,*1,:))->andover;
22          my $redundant=($repetitions&($repetitions->xvals < $repetitions->yvals))->orover;
23          my $keep_indices=which(!$redundant);
24          say +$input->mv(-1,0)->index1d($keep_indices)->mv(0,-1);
25      }
26      catch ($m) {
27          say "\n$m\nInvalid ndarray: $_";
28      }
29  }
30

Examples, including vectors, matrices, scalars and an error:

./ch-1.pl "[[1,2], [3,4], [5,6], [1,2]]" "[[9,1], [3,7], [2,5], [2,5]]" \
    "[[[1,2],[3,4]],[[5,6],[7,8]], [[5,6],[7,8]], [[9,10],[11,12]]]" \
    "[0,1,0,2,3,0,2]" "[1,a,3]"

Results:

[
 [1 2]
 [3 4]
 [5 6]
]


[
 [9 1]
 [3 7]
 [2 5]
]


[
 [
  [1 2]
  [3 4]
 ]
 [
  [5 6]
  [7 8]
 ]
 [
  [ 9 10]
  [11 12]
 ]
]

[0 1 2 3]

PDL::Core::new_pdl_from_string: found disallowed character(s) 'a' in '[1,a,3]', value now: '[1,a,3]' at ./ch-1.pl line 21.

Invalid ndarray: [1,a,3]

Task 2: Date Difference

Submitted by: Mohammad S Anwar
You are given two dates, $date1 and $date2 in the format YYYY-MM-DD.

Write a script to find the difference between the given dates in terms on years and days only.

Example 1
Input: $date1 = '2019-02-10'
   $date2 = '2022-11-01'
Output: 3 years 264 days
Example 2
Input: $date1 = '2020-09-15'
   $date2 = '2022-03-29'
Output: 1 year 195 days
Example 3
Input: $date1 = '2019-12-31'
   $date2 = '2020-01-01'
Output: 1 day
Example 4
Input: $date1 = '2019-12-01'
   $date2 = '2019-12-31'
Output: 30 days
Example 5
Input: $date1 = '2019-12-31'
   $date2 = '2020-12-31'
Output: 1 year
Example 6
Input: $date1 = '2019-12-31'
   $date2 = '2021-12-31'
Output: 2 years
Example 7
Input: $date1 = '2020-09-15'
   $date2 = '2021-09-16'
Output: 1 year 1 day
Example 8
Input: $date1 = '2019-09-15'
   $date2 = '2021-09-16'
Output: 2 years 1 day

Manipulating dates is quite complex, so I rely on the DateTime and Date::Parse packages. I’m not convinced my use is the best, but it fits a 3-liner.

Example 1:

perl -MDateTime -MDate::Parse -E '
($d1,$d2)=map {DateTime->from_epoch($_)} sort {$a<=>$b} map {str2time($_)}@ARGV;
$y=$d2->subtract_datetime($d1)->years; $d2->subtract(years=>$y);
($d)=$d2->delta_days($d1)->in_units("days");say "$y years, $d days";
' 2019-02-10 2022-11-01

Result:

3 years, 264 days

Example 2:

perl -MDateTime -MDate::Parse -E '
($d1,$d2)=map {DateTime->from_epoch($_)} sort {$a<=>$b} map {str2time($_)}@ARGV;
$y=$d2->subtract_datetime($d1)->years; $d2->subtract(years=>$y);
($d)=$d2->delta_days($d1)->in_units("days");say "$y years, $d days";
' 2020-09-15 2022-03-29

Result:

1 years, 195 days

Example 3:

perl -MDateTime -MDate::Parse -E '
($d1,$d2)=map {DateTime->from_epoch($_)} sort {$a<=>$b} map {str2time($_)}@ARGV;
$y=$d2->subtract_datetime($d1)->years; $d2->subtract(years=>$y);
($d)=$d2->delta_days($d1)->in_units("days");say "$y years, $d days";
' 2019-12-31 2020-01-01

Result:

0 years, 1 days

Example 4:

perl -MDateTime -MDate::Parse -E '
($d1,$d2)=map {DateTime->from_epoch($_)} sort {$a<=>$b} map {str2time($_)}@ARGV;
$y=$d2->subtract_datetime($d1)->years; $d2->subtract(years=>$y);
($d)=$d2->delta_days($d1)->in_units("days");say "$y years, $d days";
' 2019-12-01 2019-12-31

Result:

0 years, 30 days

Example 5:

perl -MDateTime -MDate::Parse -E '
($d1,$d2)=map {DateTime->from_epoch($_)} sort {$a<=>$b} map {str2time($_)}@ARGV;
$y=$d2->subtract_datetime($d1)->years; $d2->subtract(years=>$y);
($d)=$d2->delta_days($d1)->in_units("days");say "$y years, $d days";
' 2019-12-31 2020-12-31

Result:

1 years, 0 days

Output: 1 year

Example 6:

perl -MDateTime -MDate::Parse -E '
($d1,$d2)=map {DateTime->from_epoch($_)} sort {$a<=>$b} map {str2time($_)}@ARGV;
$y=$d2->subtract_datetime($d1)->years; $d2->subtract(years=>$y);
($d)=$d2->delta_days($d1)->in_units("days");say "$y years, $d days";
' 2019-12-31 2021-12-31

Result:

2 years, 0 days

Example 7:

perl -MDateTime -MDate::Parse -E '
($d1,$d2)=map {DateTime->from_epoch($_)} sort {$a<=>$b} map {str2time($_)}@ARGV;
$y=$d2->subtract_datetime($d1)->years; $d2->subtract(years=>$y);
($d)=$d2->delta_days($d1)->in_units("days");say "$y years, $d days";
' 2020-09-15 2021-09-16

Result:

1 years, 1 days

Example 8:

perl -MDateTime -MDate::Parse -E '
($d1,$d2)=map {DateTime->from_epoch($_)} sort {$a<=>$b} map {str2time($_)}@ARGV;
$y=$d2->subtract_datetime($d1)->years; $d2->subtract(years=>$y);
($d)=$d2->delta_days($d1)->in_units("days");say "$y years, $d days";
' 2019-09-15 2021-09-16

Results:

2 years, 1 days

The code produced the expected results in all the given examples.

Thus, with more confidence I proceed to the full code.

 1  # Perl weekly challenge 183
 2  # Task 2:  Date Difference
 3  #
 4  # See https://wlmb.github.io/2022/09/20/PWC183/#task-2-date-difference
 5  use v5.36;
 6  use experimental qw(try for_list);
 7  use DateTime;
 8  use Date::Parse;
 9  die <<FIN unless @ARGV and @ARGV%2==0;
10  Usage: $0 D1 D2 ...
11  Print the date difference between dates D1 and D2, each in the format YYYY-MM-DD.
12  Additional pairs of dates may be added;
13  FIN
14
15  for my ($date1, $date2)(@ARGV){
16      try {
17           my ($dt1, $dt2)=map {DateTime->from_epoch($_)} # ordered datetimes
18                           sort {$a<=>$b}
19                           map {str2time($_)//die "Wrong date: $_"} ($date1, $date2);
20           my $years=$dt2->subtract_datetime($dt1)->years; # year difference
21           $dt2->subtract(years=>$years); # reduce difference to within a year
22           my $days=$dt2->delta_days($dt1)->in_units("days"); # remaining days
23           my $years_name=$years==1?"year":"years"; # singular or plural
24           my $days_name=$days==1?"day":"days";
25           say "The difference between $date1 and $date2 is $years $years_name and $days $days_name";
26      }
27      catch($m){
28          say "Failed with $date1 and $date2\n$m";
29      }
30  }

Examples:

./ch-2.pl 2019-02-10 2022-11-01    2020-09-15 2022-03-29    2019-12-31 2020-01-01 \
          2019-12-01 2019-12-31    2019-12-31 2020-12-31    2019-12-31 2021-12-31 \
          2020-09-15 2021-09-16    2019-09-15 2021-09-16

Results:

The difference between 2019-02-10 and 2022-11-01 is 3 years and 264 days
The difference between 2020-09-15 and 2022-03-29 is 1 year and 195 days
The difference between 2019-12-31 and 2020-01-01 is 0 years and 1 day
The difference between 2019-12-01 and 2019-12-31 is 0 years and 30 days
The difference between 2019-12-31 and 2020-12-31 is 1 year and 0 days
The difference between 2019-12-31 and 2021-12-31 is 2 years and 0 days
The difference between 2020-09-15 and 2021-09-16 is 1 year and 1 day
The difference between 2019-09-15 and 2021-09-16 is 2 years and 1 day
Written on September 20, 2022