Perl Weekly Challenge 137: Long Year

by Abigail

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.

For more information about Long Year, please refer to Wikipedia.

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

Discussion

No year contains 53 full weeks, and all years contain 52 weeks and some days. So, the one line definition of Long Year needs some clearification.

This refers to how ISO numbers weeks. ISO weeks start on Mondays, ending on Sundays, and written as yyyyWnn, where yyyy is the year, and nn the week number (W is just the letter W).

The first week of the year is the first week which has at least four days in January. This means that the last days of December could be part of week 01 of the next year, or that the first days of January could be part of the last week of the previous year. (This means that at the end or the beginning of the year, the yyyy part of the ISO week number the date falls into may not equal the current year).

Since years are slightly longer than 52 weeks, every now and then we have a year with a week 53.

In particular, a year has a 53rd week if one of the conditions is true:

The Gregorian calendar repeats

The cycle of leap years in the Gregorian calendar repeats every 400 years. 400 years in the Gregorian calendar means 146097 days (400 times 365 days plus 97 leap days). And 146097 = 7 * 20871, so 400 years have an exact number of weeks.

This means that the cycle of Long Years repeats every 400 years. So, if we have the Long Years in a 400 year period, we can easily expand this to get all the Long Years. There are 71 Long Years in each 400 year period. For the period 2000 to 2399, we have the following Long Years:

2004 2009 2015 2020 2026 2032 2037 2043 2048
2054 2060 2065 2071 2076 2082 2088 2093 2099
2105 2111 2116 2122 2128 2133 2139 2144 2150
2156 2161 2167 2172 2178 2184 2189 2195
2201 2207 2212 2218 2224 2229 2235 2240 2246
2252 2257 2263 2268 2274 2280 2285 2291 2296
2303 2308 2314 2320 2325 2331 2336 2342 2348
2353 2359 2364 2370 2376 2381 2387 2392 2398

To find out whether a year before 2000 or after 2400 is a Long Year, just add or substract the appropriate multiple of 400 and check the table.

Live Demo

Enter a year in the (proleptic) Gregorian calendar, and hit the Calculate button to check whether the year in a Long Year or not.

Enter a year:

Solutions

Our main solution takes a list of offsets of Long Years for 400 year periods starting at years \(400 \cdot k\). We calculate all the Long Years in the 400 year periods starting in 1600 and 2000 (so we get all the Long Years from 1600 to 2400), and then selecting the years between 1900 and 2100.

For a select few languages, we have implemented an alternative solution. This solution calculates the day of the week of December 31 of the year. If that day is a Thursday, or if that day is a Friday, and the year is a leap year, the year is a Long Year.

Perl

Main solution

my @start_years       = qw [1600 2000];
my @long_year_offsets = qw [004 009 015 020 ... 387 392 398]; # All 71 offsets

say for grep {1900 <= $_ <= 2100}
         map {my $fy = $_; map {$_ + $fy} @long_year_offsets} @start_years

This uses a nested map to get all the possible sums of a start year and an offset, then greps the ones between 1900 and 2100, which are printed.

Find the full program on GitHub.

Alternative solution

First, we calculate the doomsday of the year. This is the day of the week certain days in a year all share: 4/4, 6/6, 8/8, 10/10, 12/12, 5/9, 9/5, 7/11, 11/7, Pi Day, and the last day of Februari, using an algorithm devised by John Conway:

sub doomsday ($year) {
    use integer;
    my $anchor   = (2, 0, 5, 5) [($year / 100) % 4];
    my $y        = $year % 100;
    my $doomsday = ((($y / 12) + ($y % 12) + (($y % 12) / 4)) + $anchor) % 7;
    $doomsday;
}

The return value is a integer 0 to 6, with 0 indicating Sunday, and 6 a Saturday.

This gives us the day of the week of December 12. To calculate the day of the week of December 31, we subtract 12, add 31, and take this modulo 7. All we have to do is check if it's a Thursday, or a Friday and a leap year:

foreach my $year (1900 .. 2100) {
    my $doomsday = doomsday ($year);
    my $dec_31   = ($doomsday - 12 + 31) % 7;
    say $year if $dec_31 == 4 || $dec_31 == 5 && is_leap $year;
}

with the following code to determine whether a year is a leap year:

sub is_leap ($year) {
    ($year % 400 == 0) || ($year % 4 == 0) && ($year % 100 != 0)
}

Find the full program on GitHub.

Go

Our Go solution looks very similar to the main Perl solution, except that we use a nested for loop to calculate the Long Years:

func main () {
    start_years       := [] int {1600, 2000}
    long_year_offsets := [] int {4, 9, 15, ..., 387, 392, 398};

    for _, start_year := range start_years {
        for _, offset := range long_year_offsets {
            year := start_year + offset
            if 1900 <= year && year <= 2100 {
                fmt . Println (year)
            }
        }
    }
}

Find the full program on GitHub.

R

The R solution uses nested for loops as well, but it uses a trick R has: if you add a number to a vector, the result is a vector with the number added to each element of the original vector. This gives us:

start_years       <- c (1600, 2000)
long_year_offsets <- c (4, 9, 15, ..., 387, 392, 398)

for (start_year in start_years) {
    for (year in start_year + long_year_offsets) {
        if (1900 <= year && year <= 2100) {
            cat (year, "\n")
        }
    }
}

Find the full program on GitHub.

Erlang

Erlang is a functional language, so we have to write our loops in a functional way:

long_year ([], _) -> ok;
long_year (_, []) -> ok;

long_year ([Start], [Offset | Offsets]) ->
    Year = Start + Offset,
    if (1900 =< Year) and (Year =< 2100) -> io:fwrite ("~B~n", [Year]);
        true -> ok
    end,
    long_year ([Start], Offsets);

long_year ([Start | Starts], Offsets) ->
    long_year ([Start], Offsets),
    long_year (Starts, Offsets).

Find the full program on GitHub.

Other Languages

We also have solutions in: AWK, Bash, bc, C, Java, Lua, Node.js, Pascal, Python, Ruby, Scheme, and Tcl.


Please leave any comments as a GitHub issue.