TL;DR

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

The challenge

Write a script to find all the years between 1900 and 2100 which is a Long Year.

A year is Long if it has 53 weeks.

Expected Output

1903, 1908, 1914, 1920, 1925,
1931, 1936, 1942, 1948, 1953,
1959, 1964, 1970, 1976, 1981,
1987, 1992, 1998, 2004, 2009,
2015, 2020, 2026, 2032, 2037,
2043, 2048, 2054, 2060, 2065,
2071, 2076, 2082, 2088, 2093,
2099

The questions

The first question being… what the heck is a long year? Well, it’s already answered: it’s a year with 53 weeks.

The meta answer in this case is: do your own research. Assuming that we stick to the ISO standardization here, we come to understand that:

  • a week starts on monday;
  • a week belongs to a specific year if most of its days fall within that year.

So… this is all we need to know! More in ISO week date.

The solution

Section Weeks per year in ISO week date contains the following simple characterization for long years:

years in which 1 January or 31 December are Thursdays

This is nice, but slightly inefficient.

Any non-leap year has an interesting characteristic: the first day and the last day occur on the same week day! Hence, for non-leap years, having the January 1st fall on Thursday means that December 31st does that too. Which means: just check January 1st!

On the other hand, leap years might have December 31st to fall on a Thursday even if January 1st falls on a Wednesday.

So an algorithm might be the following:

  • calculate the day of the week for January 1st, let’s say it’s $dow;
  • if it’s a Thursday, our year is long;
  • otherwise, if it’s not a Wednesday it is not long;
  • otherwise, we check December 31st to be a Thursday.

Everything clear?

Raku goes first:

#!/usr/bin/env raku
use v6;

subset FullyGregorianYear of Int where * > 1582;
sub is-long-year (FullyGregorianYear:D $y) {
   my $dow = Date.new($y, 1, 1).day-of-week;
   return $dow == 4 || $dow == 3 && Date.new($y, 12, 31).day-of-week == 4;
}

my @longs = (1900 .. 2100).grep({is-long-year($_)});
while @longs > 0 {
   my @slice = @longs.splice(0, 5);
   @slice.push: '' if @slice == 5;
   @slice.join(', ').put;
}

The day-of-week function returns 3 for Wednesdays and 4 for Thursdays.

Calculating $dow allows us to check it first against Thursdays and, if not, against Wednesdays. This spares us some calculation.

The rest of the code is just to re-create the fancy layout of the expected output.

Perl now, in what is mostly a translation:

#!/usr/bin/env perl
use v5.24;
use warnings;
use experimental 'signatures';
no warnings 'experimental::signatures';
use Time::Local 'timegm';

sub dow ($y, $m, $d) { (gmtime(timegm(1, 1, 1, $d, --$m, $y)))[6] }
sub is_long_year ($y) {
   my $dow = dow($y, 1, 1);
   return $dow == 4 || $dow == 3 && dow($y, 12, 31) == 4;
} ## end sub is_long_year ($y)

my @longs = grep { is_long_year($_) } (1900 .. 2100);
while (@longs > 0) {
   my @slice = splice @longs, 0, 5;
   say join ', ', @slice, (@slice == 5 ? '' : ());
}

This time we could go for DateTime but it’s overkill and we can do with CORE stuff only, using Time::Local. Function dow() calculates the day of week and has a twist: we MUST use the full year, instead of the offset with respect to 1900 (like localtime). The documentation has all the details, but the TL;DR is to just stick to the full year value.

OK, enough for this challenge… stay safe folks!