Peter
Peter Campbell Smith

Seize the greatness

Weekly challenge 237 — 9 October 2023

Week 237 - 9 Oct 2023

Task 1

Task — Seize the day

Given a year, a month, a week of the month, and a day of the week (1 (Mon) .. 7 (Sun)), print the day.

Examples


Example 1
Input: Year = 2024, Month = 4, Weekday of month = 3, day of week = 2
Output: 16

The 3rd Tue of Apr 2024 is the 16th

Example 2
Input: Year = 2025, Month = 10, Weekday of month = 2, day of week = 4
Output: 9

The 2nd Thu of Oct 2025 is the 9th

Example 3
Input: Year = 2026, Month = 8, Weekday of month = 5, day of week = 3
Output: 0

There isn't a 5th Wed in Aug 2026

Analysis

It would be easiest to do this using one of the date/time modules, but I perversely did it without.

My approach, using as an example the data for Example 1 above, is:

  • Get the day of the week of the 1st of January 2024 (see below)
  • From that get the day of the week of the 1st of April 2024
  • From that get the date of first Tuesday of April 2024
  • From that get the date of the 3rd Tuesday

To get the day of the week of the 1st of January 2024 I start with the knowledge that 1 January 1753 (the first full year of the Gregorian Calendar) was a Monday. I then cycle through the years from 1753 to 2024 adding 1 day for each non-leap year (eg Monday → Tuesday) and 2 for every leap year (eg Monday → Wednesday).

The last step is to check whether the day exists - for example the 32nd of April doesn't - and to respond appropriately.

As you will see from my sample output this works for any date from 1753 to 9999 and beyond, up to the largest year number your system can handle, though it will take rather a long time for very large numbers.

You will see that the 5th Friday of December 9999 is the 31st, which will give us the weekend to update all our systems to handle 5-digit years.

Try it 

Try running the script with any input, for example:
2023, 10, 2, 1


Script


#!/usr/bin/perl

use v5.16;    # The Weekly Challenge - 2023-10-02
use utf8;     # Week 237 task 1 - Seize the day
use strict;   # Peter Campbell Smith
use warnings; # Blog: http://ccgi.campbellsmiths.force9.co.uk/challenge

seize_the_day(2024, 4, 3, 2);
seize_the_day(2025, 10, 2, 4);
seize_the_day(2026, 8, 5, 3);
seize_the_day(1947, 11, 2, 7);
seize_the_day(2024, 2, 5, 4);
seize_the_day(9999, 12, 5, 5);
seize_the_day(1753, 1, 1, 1);
seize_the_day(2023, 10, 2, 1);

sub seize_the_day {
    
    my ($year, $month, $week, $day, @days, @months, @days_in_month, @suffix,
        $dow, $dom, $result);
    
    ($year, $month, $week, $day) = @_;
    
    # sanity check
    say qq[\nInput:  \$year = $year, \$month = $month, \$week = $week, \$day = $day];
    if ($year < 1753 or $month < 1 or $month > 12 or $week < 0 or $day < 1 or $day > 7) {
        say qq[Output: bad data];
        return;
    }
    
    # initialise
    @days = qw[Sun Mon Tues Wednes Thurs Fri Satur];
    @months = qw[x January February March April May June July August September October November December];
    @days_in_month = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
    $days_in_month[2] += 1 if is_leap($year);
    push @suffix, 'th' for (0 .. 31);
    $suffix[$1] = $2 while '1st 2nd 3rd 21st 22nd 23rd 31st' =~ m|(\d+)([a-z]{2})|g;
    $day = $day % 7;
    
    # get day of the week for 1 Jan $year
    $dow = -1;
    $dow += is_leap($_- 1) ? 2 : 1 for 1753 .. $year;
    
    # get day of week for 1 $month $year
    if ($month > 1) {
        $dow += $days_in_month[$_] for (0 .. $month - 1);
    }
    $dow = $dow % 7;
    
    # get day of month for first $day in the month
    $dom = ($day - $dow + 7) % 7 + 1;
    
    # get day of month for $week-th $day in the month
    $dom += ($week - 1) * 7;
    
    # does it exist?
    if ($dom > $days_in_month[$month]) {
        say qq[Output: There is no $week$suffix[$week] $days[$day]day in $months[$month] $year];
    } else {
        say qq[Output: The $week$suffix[$week] $days[$day]day of $months[$month] $year is the $dom$suffix[$dom]];
    }
}

sub is_leap {  # returns 1 for leap year, 0 for non-leap
    
                         # non-century year   :  century year 
    return ($_[0] % 100 ? ($_[0] % 4 ? 0 : 1) : ($_[0] % 400 ? 0 : 1));
}

Output


Input:  $year = 2024, $month = 4, $week = 3, $day = 2
Output: The 3rd Tuesday of April 2024 is the 16th

Input:  $year = 2025, $month = 10, $week = 2, $day = 4
Output: The 2nd Thursday of October 2025 is the 9th

Input:  $year = 2026, $month = 8, $week = 5, $day = 3
Output: There is no 5th Wednesday in August 2026

Input:  $year = 1947, $month = 11, $week = 2, $day = 7
Output: The 2nd Sunday of November 1947 is the 9th

Input:  $year = 2024, $month = 2, $week = 5, $day = 4
Output: The 5th Thursday of February 2024 is the 29th

Input:  $year = 9999, $month = 12, $week = 5, $day = 5
Output: The 5th Friday of December 9999 is the 31st

Input:  $year = 1753, $month = 1, $week = 1, $day = 1
Output: The 1st Monday of January 1753 is the 1st

Input:  $year = 2023, $month = 10, $week = 2, $day = 1
Output: The 2nd Monday of October 2023 is the 9th