PWC227 - Friday 13th

TL;DR

Here we are with TASK #1 from The Weekly Challenge #227. Enjoy!

The challenge

You are given a year number in the range 1753 to 9999.

Write a script to find out how many dates in the year are Friday 13th, assume that the current Gregorian calendar applies.

Example

Input: $year = 2023
Output: 2

Since there are only 2 Friday 13th in the given year 2023 i.e. 13th Jan and 13th Oct.

The questions

My initial question was why 1753?. It might be related to the fact that the Gregorian calendar was adopted in 1752, so it’s to be on the safe side. Well, actually, it seems that they adopted something equivalent, right?

Then there is the choice of the upper limit for the range. Don’t get me wrong, I’m happy there is a range, only curiosity.

The solution

We will start with Raku as usual, also because it provides the most sane solution of what we’re going to see here:

#!/usr/bin/env raku
use v6;
sub MAIN (*@years) { @years.map({ put $_, ' ', friday_13th($_) }) }

sub friday_13th ($year) {
   (1..12).grep({ Date.new($year, $_, 13).day-of-week == 5 }).elems
}

I claim it’s the most sane of them all because it’s easy to see what’s going on: an official Date object is initialized, the day-of-week is extracted and compared to 5, which means friday. Well, maybe we might get rid of the 5 as a magic number and make it more explicit, but whatever.

My initial scaffolding for a Perl counterpart was to try and make it readable too:

#!/usr/bin/env perl
use v5.24;
use warnings;
use experimental 'signatures';

say $_, ' ', friday_13th($_) for @ARGV;

sub friday_13th ($year) {
   return scalar grep { dow($year, $_, 13) == 5 } 1 .. 12;
}

So we moved the juicy part into function dow(), which is supposed to receive a date and give back a day of the week (with the usual convention that Sunday is 0, Monday is 1, etc.)

OK, how to implement it? My first take was to rely on Perl’s gmtime and Time::Local, which are in CORE. From the date we get to an epoch, then back to a date with the additional bit of information we’re after:

sub dow_timegm ($y, $m, $d) {
   require Time::Local;
   my $epoch = Time::Local::timegm_modern(30, 30, 12, $d, $_ - 1, $y);
   return (gmtime($epoch))[6];
}

One drawback of this approach is that, officially speaking, the epoch starts in 1970 and using it as early as 1753 is a bit of an abuse (we’re assuming that we will not be hit by the 2038).

Anyway, I checked and the results are the same as the Raku alternative, over the whole range. So I confidently say: it works for me!

And yet, if we don’t still feel completely confident, we can look around and land on the following:

# https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week
sub dow_algorithm ($y, $m, $d) {
   state $t = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
   use integer;
   --$y if $m < 3;
   return ($y + $y / 4 - $y / 100 + $y / 400 + $t->[$m - 1] + $d) % 7;
}

This is the least readable of them all, but it works.

We just miss some sugar to complete the program:

sub dow ($y, $m, $d) {
   state $calculator = $ENV{DOW_TIMEGM} ? \&dow_timegm : \&dow_algorithm;
   return $calculator->($y, $m, $d);
}

So there you go, either understand it at the risk of missing the point in some machines that are not mine, or get the correct result but out of faith in the code.

Yes, we could go the full DateTime solution, but it installs 34 MB of stuff and it seems a little overkill for such a simple task. It gives us the readability of the Raku solution, though.

Cheers!


Comments? Octodon, , GitHub, Reddit, or drop me a line!