This is my response to The Weekly Challenge #183.
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]]
#! /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].
$date1
and $date2
in the format
YYYY-MM-DD
.
years
and days
only.
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.
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.
See
docs.raku.org/type/Date
for more information about the Date
class.
#! /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.