ETOOBUSY 🚀 minimal blogging for the impatient
PWC183 - Date Difference
TL;DR
On with TASK #2 from The Weekly Challenge #183. Enjoy!
The challenge
You are given two dates,
$date1
and$date2
in the formatYYYY-MM-DD
.Write a script to find the difference between the given dates in terms on
years
anddays
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
The questions
Why on earth are we mixing years and days without giving a proper specification about what we’re expecting back?
OK, we’re going to assume that the following is correct:
- advance date1 for as many years as the difference in years between the two dates
- if the landing date1 does not exist (Feb 29th in a non-leap year) go back one day
- if the landing date1 comes after date2, go back one year
- count the number of days
The solution
I hate this kind of date processing stuff, especially for such under-specified situations. If there’s anything I learned is that any serious date business MUST be done with the right library, and this is definitely something that a library does not help with.
In addition, I generally hate having to deal with date fiddling.
Last, I hate having to hate.
So I’m doing this only for manwar, who is a nice person.
For this reason, this is my Perl solution:
#!/usr/bin/env perl
use v5.24;
use warnings;
use experimental 'signatures';
no warnings 'experimental::signatures';
use Time::Local 'timegm';
use Test::More;
my @tests = qw<
2019-02-10 2022-11-01 3 264
2020-09-15 2022-03-29 1 195
2019-12-31 2020-01-01 0 1
2019-12-01 2019-12-31 0 30
2019-12-31 2020-12-31 1 0
2019-12-31 2021-12-31 2 0
2020-09-15 2021-09-16 1 1
2019-09-15 2021-09-16 2 1
2019-02-28 2020-02-27 0 364
2019-02-28 2020-02-28 1 0
2019-02-28 2020-02-29 1 1
2019-02-28 2020-03-01 1 2
2019-03-01 2020-02-28 0 364
2019-03-01 2020-02-29 0 365
2019-03-01 2020-03-01 1 0
2020-02-28 2021-02-27 0 365
2020-02-28 2021-02-28 1 0
2020-02-28 2021-03-01 1 1
2020-02-29 2021-02-28 0 365
2020-02-29 2021-03-01 1 1
>;
while (@tests) {
my ($date1, $date2, $exp_years, $exp_days) = splice @tests, 0, 4;
my ($got_years, $got_days) = years_and_days($date1, $date2);
is $got_years, $exp_years, "($date1 vs $date2) years OK";
is $got_days, $exp_days, "($date1 vs $date2) days OK";
}
done_testing();
sub years_and_days ($date1, $date2) {
my %parts_of;
for my $date ($date1, $date2) {
complain($date, 'undefined') unless defined $date;
my @pts = $date =~ m{\A ([1-9]\d{3})-(\d\d)-(\d\d) \z}mxs
or complain($date, 'no match');
complain($date, 'too low') unless $pts[0] > 1585;
$_ += 0 for @pts;
$parts_of{$date} = {
year => $pts[0],
in_year => sprintf('%02d-%02d', $pts[1], $pts[2]),
month => $pts[1],
day => $pts[2],
};
date_exists($parts_of{$date}) or complain($date, 'inexistent');
# OK, the date is valid
}
complain($date2, 'too early') if $date2 lt $date1;
($date1, $date2) = @parts_of{$date1, $date2};
# first rought evaluation of years between
my $years = $date2->{year} - $date1->{year};
my $days = undef;
# if the "in year" day of the first date comes before the corresponding
# in the second one, just count the days in between
if ($date1->{in_year} le $date2->{in_year}) {
set_year($date1, $date2->{year});
$days = days_diff($date1, $date2);
}
# otherwise we have to "lend a year" and calculated days across the
# change of the year by setting the first date in the previous year
else {
--$years;
set_year($date1, $date2->{year} - 1);
$days = days_diff($date1, $date2);
}
return ($years, $days);
}
sub epc ($d) { timegm(30, 30, 12, $d->{day}, $d->{month} - 1, $d->{year}) }
sub date_exists ($date) { eval { epc($date); 1 } }
sub set_year ($date, $year) {
$date->{year} = $year;
# We might land a valid starting Feb 29th onto an invalid Feb 29th so we
# might have to adjust
$date->@{qw< day in_year >} = (28, '02-28') unless date_exists($date);
return $date;
}
sub days_diff ($date1, $date2) {
my $epoch1 = epc($date1);
my $epoch2 = epc($date2);
return int(($epoch2 + 3600 - $epoch1) / (24 * 3600));
}
sub complain ($date, $reason) {
die "invalid date: $reason\n" unless defined $date;
die "invalid date '$date': $reason\n";
}
No Raku this time, because I don’t want to lose the -Ofun
!
Stay safe and… relaxed!