Unique Difference
with Raku

by Arne Sommer

Unique Difference with Raku and Perl

[203] Published 25. September 2022.

This is my response to The Weekly Challenge #183.

Challenge #183.1: Unique Array

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])

Raku does not have arrayrefs, but real multidimentional arrays are much better.

File: unique-array
#! /usr/bin/env raku

unit sub MAIN (:v(:$verbose));

my @list1 = ([1,2], [3,4], [5,6], [1,2]);  # [1]
my @list2 = ([9,1], [3,7], [2,5], [2,5]);  # [1]

say unique-array(@list1);                  # [2]
say unique-array(@list2);                  # [2]

sub unique-array (@list)                   # [3]
{
  my @unique;                              # [4]
  my %seen;                                # [5]
  
  for @list -> $ref                        # [6]
  {
    next if %seen{$ref};                   # [7]
    @unique.push: $ref;                    # [8]
    %seen{$ref}++;                         # [9]
  }

  say ": Seen: " ~ %seen.raku if $verbose;
  return @unique;                          # [10]
}

[1] I have chosen to hard code the arrays.

[2] Then we call the procedure on each one, printing the result.

[3] The procedure.

[4] The return value will end up here; the list of unique sublists.

[5] A list of sublist seen so far.

[6] Iterate over the sublists. (Do not let the variable name «$ref» fool you; this is an array.)

[7] Skip sublists we have already seen. Note that using an array as a key will stringify it. The result is the two values with a space character between them.

[8] Add the sublist to the result.

[9] Mark the sublist as seen.

[10] Return the unique array.

Running it:

$ ./unique-array
[[1 2] [3 4] [5 6]]
[[9 1] [3 7] [2 5]]

We got brackets all the way, instead of only for the sublists. That is probably ok.

With verbose mode:

$ ./unique-array -v
: Seen: {"1 2" => 1, "3 4" => 1, "5 6" => 1}
[[1 2] [3 4] [5 6]]
: Seen: {"2 5" => 1, "3 7" => 1, "9 1" => 1}
[[9 1] [3 7] [2 5]]

A Perl Version

This is straight forward translation of the Raku version.

File: unique-array-perl
#! /usr/bin/env perl

use strict;
use warnings;
use feature 'say';
use feature 'signatures';

no warnings 'experimental::signatures';

my @list1 = ([1,2], [3,4], [5,6], [1,2]);
my @list2 = ([9,1], [3,7], [2,5], [2,5]);

say "(", join(", ", map { "[$_->[0],$_->[1]]" } unique_array(@list1)), ")";  # [1]
say "(", join(", ", map { "[$_->[0],$_->[1]]" } unique_array(@list2)), ")";  # [1]

sub unique_array (@list)
{
  my @unique;
  my %seen;
  
  for my $ref (@list)
  {
    next if $seen{"$ref->[0] $ref->[1]"};                                    # [1]
    push @unique, $ref;
    $seen{"$ref->[0] $ref->[1]"}++;                                          # [1]
  }

  say ": Seen: %seen" if $verbose;
  return @unique;
}

[1] Stringification of an arrayref does not work (the result is a string like this: ARRAY(0x56073743b5f0), where the first part is the type, and the second the memory location. That location will obviously not be the same for different objects - evenif the values turn out to be the same. So we have to do the stringification manually.

Running it gives the same result as the Raku version, except that we got the brackets right this time - as we printed them manually with «map»).

$ ./unique-array-perl 
([1,2], [3,4], [5,6])
([9,1], [3,7], [2,5])

Verbose mode has gone, as it is difficult to print these structures, as shown by [1].

Challenge #183.2:

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.

Examples
Input: $date1 = '2019-02-10'
       $date2 = '2022-11-01'
Output: 3 years 264 days

Input: $date1 = '2020-09-15'
       $date2 = '2022-03-29'
Output: 1 year 195 days

Input: $date1 = '2019-12-31'
       $date2 = '2020-01-01'
Output: 1 day

Input: $date1 = '2019-12-01'
       $date2 = '2019-12-31'
Output: 30 days

Input: $date1 = '2019-12-31'
       $date2 = '2020-12-31'
Output: 1 year

Input: $date1 = '2019-12-31'
       $date2 = '2021-12-31'
Output: 2 years

Input: $date1 = '2020-09-15'
       $date2 = '2021-09-16'
Output: 1 year 1 day

Input: $date1 = '2019-09-15'
       $date2 = '2021-09-16'
Output: 2 years 1 day

Placing the dates in a text file, one set per row, gives us an easy way of supplying them to the program.

File: dates.txt
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
2023-01-01 2023-01-01

I have added the last one.

Perl

Let us start with the Perl version this time.

There are more than one Date module, but Date::Calc - which I used two weeks ago (in Hot Sentence with Raku and Perl) - works out here as well.

File: date-difference-perl
#! /usr/bin/env perl

use strict;
use warnings;
use feature 'say';
use File::Slurp;
use Date::Calc qw/N_Delta_YMD Delta_Days/;
use feature 'signatures';

no warnings 'experimental::signatures';

my $file = shift(@ARGV) || "dates.txt";
my @rows = read_file($file, chomp => 1);                   # [1]

for my $row (@rows)                                        # [2]
{
  my ($date1, $date2) = split(/\s+/, $row);                # [3]

  say "$date1 vs $date2 -> " . date_diff($date1, $date2);  # [4]
}


sub date_diff ($date1, $date2)
{
  ($date1, $date2)   = ($date2, $date1) if $date1 gt $date2;                 # [5]
  my ($y, $m, $d)    = N_Delta_YMD(split("-", $date1), split("-", $date2));  # [6]
  my ($y2, $m2, $d2) = split("-", $date1);                                   # [7]
  my $days           = Delta_Days($y2 + $y, $m2, $d2, split("-", $date2));   # [8]
  my @return;                                                                # [9]

  push(@return, plural("year", $y))   if $y;                                 # [10]
  push(@return, plural("day", $days)) if $days;                              # [11]

  return "0 days" unless @return;                                            # [12]
    
  return join(" ", @return);                                                 # [13]
}

sub plural ($label, $val)
{
  return "$val $label" if $val == 1;                                         # [14]
  return "$val $label". "s";                                                 # [14a]
}

[1] «read_file» is supplied by «File::Slurp».

[2] For each row in the file,

[3] Get the start and end dates.

[4] Print the dates, and the difference.

[5] Ensure that the first date is the lowest.

[6] Get the difference between the two dates, in days, months and days.

[7] Get the parts (year, month, day) of the first date.

[8] Add the number of years (from [6]), and then get the difference in days.

[9] The return value.

[10] Add the day(s) part, if any.

[11] Add the year(s) part, if any.

[12] The same day? Say so.

[13] Return the year(s) and day(s).

[14] Add the plural postfix «s» where applicable.

Running it:

$ ./date-difference-perl 
2019-02-10 vs 2022-11-01 -> 3 years 264 days
2020-09-15 vs 2022-03-29 -> 1 year 195 days
2019-12-31 vs 2020-01-01 -> 1 day
2019-12-01 vs 2019-12-31 -> 30 days
2019-12-31 vs 2020-12-31 -> 1 year
2019-12-31 vs 2021-12-31 -> 2 years
2020-09-15 vs 2021-09-16 -> 1 year 1 day
2019-09-15 vs 2021-09-16 -> 2 years 1 day
2023-01-01 vs 2023-01-01 -> 0 days

Looking good.

Raku

This is a straight forward translation of the Perl version, using the builtin «Date» class.

See docs.raku.org/type/Date for more information about the Date class.

File: date-difference
#! /usr/bin/env raku

unit sub MAIN ($file where $file.IO.f && $file.IO.r = "dates.txt");

my @rows = $file.IO.lines;

for @rows -> $row
{
  my ($date1, $date2) = $row.words;

  say "$date1 vs $date2 -> " ~ date-diff($date1, $date2);
}

sub date-diff ($date1, $date2)
{
  ($date1, $date2)   = ($date2, $date1) if $date1 gt $date2;

  my $d1 = $date1.Date;
  my $d2 = $date2.Date;

  my $years = 0;                           # [1]

  while ( $d2 >= $d1.later(:year))         # [1a]
  {
    $years++;                              # [1b]
    $d1 = $d1.later(:year);                # [1c]
  }

  my $days = $d2.daycount - $d1.daycount;  # [2]
  my @return;
    
  push(@return, plural("year", $years)) if $years;
  push(@return, plural("day", $days))   if $days; 

  return "0 days" unless @return;
    
  return join(" ", @return); 
}

sub plural ($label, $val)
{
  return "$val $label" if $val == 1;
  return "$val $label" ~ "s";
}

[1] Count the number of years; as long as the first date is one year or more earlier than the second one [1a], add one year (to the count [1b] and the date [1c].

[2] There is, as far as I can see, no way of comparing (getting the difference) two Date objects, but the daycount method gives the number of days since the epoch - and subtracting the two daycounts give the difference in days.

Running it gives the same result as the Perl version:

$ ./date-difference
2019-02-10 vs 2022-11-01 -> 3 years 264 days
2020-09-15 vs 2022-03-29 -> 1 year 195 days
2019-12-31 vs 2020-01-01 -> 1 day
2019-12-01 vs 2019-12-31 -> 30 days
2019-12-31 vs 2020-12-31 -> 1 year
2019-12-31 vs 2021-12-31 -> 2 years
2020-09-15 vs 2021-09-16 -> 1 year 1 day
2019-09-15 vs 2021-09-16 -> 2 years 1 day
2023-01-01 vs 2023-01-01 -> 0 days

And that's it.