Perl Weekly Challenge: Week 237

Challenge 1:

Seize The Day

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

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

I love calendar problems. Perl has the best libraries for time and date problems in any major programming language in my opinion.

After getting the scripts parameters from the command-line...

my ($year, $month, $weekday, $dayofweek) = @ARGV;

...the first step is to create a DateTime object set to the first of the month specified in the year specified. For this you have to use DateTime; at the top of the script. Normally I don't use external modules in these solutions but it is well worth it in this case considering the many edge cases and gotchas in calendar maths.

my $dt = DateTime->new(year => $year, month => $month, day => 1);

We need to know the difference between the day of week on the first of the month and the day of week we are looking for. We are only interested in the magnitude of the difference not the direction so I used the abs() function to avoid a negative result.

my $diff = abs($dayofweek -  $dt->day_of_week);

Now we can calculate the date we are looking for. We subtract one from $weekday because we are going to count weeks from the 0th one not the 1st. Then we multiply that by 7 because there are seven days in a week and add the difference calculated in the last line to the result. This is the number of days to be added to the DateTime object to get the the date we want. (As we are already on the 1st of the month 1 is added.)

$dt->set(day => 1 + (7 * ($weekday - 1)) + $diff);

This will work for the first two examples but in the third, the date is invalid. In this case DateTime throws an exception. We can catch it by enclosing the line in an eval() block.

eval { $dt->set(day => 1 + (7 * ($weekday - 1)) + $diff); };

If an exception was raised it is caught by this block. Adding use English; at the top of the script allows you to use the more descriptive EVAL_ERROR instead of $@. We print an error message in the format shown in the examples.

if ($EVAL_ERROR) {
    $dt->set(day => 1 + $diff + 1);
    say "There isn't a ", $weekday, ' ', $dt->day_abbr,' in ',
        $dt->month_abbr, ' ', $year;

If there was no execption, we print the results, again, in the format shown in the examples.

} else {
    say 'The ', $weekday, ' ', $dt->day_abbr,' of ', $dt->month_abbr,
        ' ', $year, ' is the ', $dt->day;
}

My output doesn't look quite the same as the examples though because in several places the latter has the ordinal form of a number i.e. 1st, 2nd etc. Unfortunately DateTime doesn't have any way of creating them so I wrote my own function.

ordinal() looks like this:

sub ordinal {
    my ($day) = @_;
    return $day . (
        ($day % 10 == 1 && $day != 11)
        ? 'st'
        : ($day % 10 == 2 && $day != 12)
            ? 'nd'
            : ($day % 10 == 3 && $day != 13)
                ? 'rd'
                : "th"
    );
}

Now I can go back to my previous output code and improve it.

if ($EVAL_ERROR) {
    $dt->set(day => 1 + $diff + 1);
    say "There isn't a ", ordinal($weekday), ' ', $dt->day_abbr,' in ',
        $dt->month_abbr, ' ', $year;
} else {
    say 'The ', ordinal($weekday), ' ', $dt->day_abbr,' of ', $dt->month_abbr,
        ' ', $year, ' is the ', ordinal($dt->day);
}

(Full code on Github.)

This is the Raku version:

sub MAIN(
    $year, $month, $weekday, $dayofweek
) {

Raku's equivalent to DateTime is the Date class.

    my $dt = Date.new(year => $year, month => $month, day => 1);

    my $diff = abs($dayofweek -  $dt.day-of-week);

You add days to a Date object like this:

    $dt += (7 * ($weekday - 1)) + $diff;

Unlike DateTime, Date doesn't have functions for day and month abbreviations so I added my own lookup tables. This is less flexible because it is English-only instead of localizing to the users language as DateTime does via DateTime::Locale but it is good enough for the current purpose.

    my %d_abbr = (
        1 => 'Mon',
        2 => 'Tue',
        3 => 'Wed',
        4 => 'Thu',
        5 => 'Fri',
        6 => 'Sat',
        7 => 'Sun',
    );

    my %m_abbr = (
        1 => 'Jan',
        2 => 'Feb',
        3 => 'Mar',
        4 => 'Apr',
        5 => 'May',
        6 => 'Jun',
        7 => 'Jul',
        8 => 'Aug',
        9 => 'Sep',
        10 => 'Oct',
        11 => 'Nov',
        12 => 'Dec',
    );

Instead of throwing an exception when the days of a month are out of range, Date just continues on to the next month.

So if the month in $dt is not the month we specified, we print the error meesage.

    if $dt.month != $month {
        say "There isn't a ", ordinal($weekday), ' ', %d_abbr{$dayofweek},
            ' in ', %m_abbr{$month}, ' ', $year;

Else we print the result message.

    } else {
        say 'The ', ordinal($weekday), ' ', %d_abbr{$dayofweek},' of ',
            %m_abbr{$month}, ' ', $year, ' is the ', ordinal($dt.day);
    }
}

In both cases we use an ordinal() function which works exactly the same as in Perl.

sub ordinal($day) {

    return $day ~ (
        $day % 10 == 1 && $day != 11
        ?? 'st'
        !! $day % 10 == 2 && $day != 12
            ?? 'nd'
            !! $day % 10 == 3 && $day != 13
                ?? 'rd'
                !! "th"
    );
}

(Full code on Github.)

Challenge 2:

Maximize Greatness

You are given an array of integers.

Write a script to permute the give array such that you get the maximum possible greatness.

To determine greatness, nums[i] < perm[i] where 0 <= i < nums.length

Example 1
Input: @nums = (1, 3, 5, 2, 1, 3, 1)
Output: 4

One possible permutation: (2, 5, 1, 3, 3, 1, 1) which returns 4 greatness as below:
nums[0] < perm[0]
nums[1] < perm[1]
nums[3] < perm[3]
nums[4] < perm[4]
Example 2
Input: @ints = (1, 2, 3, 4)
Output: 3

One possible permutation: (2, 3, 4, 1) which returns 3 greatness as below:
nums[0] < perm[0]
nums[1] < perm[1]
nums[2] < perm[2]

A variable is defined to hold the maximum greatness score found so far. (Initially it will be 0.)

my $maxGreatness = 0;

Raku has a .permutations() method built in so it is very easy to get all permutations of the input.

for @nums.permutations -> $perm {

For each permutation, elements are compared to their corresponding element in the original input one by one. This is easy to achieve with the Z operator which takes two lists and "zips" them, successively producing pairs of elements, first the 0th ones from each list, then the 1st elements, 2nd and so on. We .grep() through these pairs looking for ones where the first element is less than the second one. .elems() counts how many such pairs were found; this is the greatness score.

    my $greatness =  (@nums Z @$perm).grep({ @$_[0] < @$_[1 ]}).elems;

If the greatness of the current permutation is greater than the current maximum greatness, it becomes the new value of $maxGreatness.

    if $greatness > $maxGreatness {
        $maxGreatness = $greatness;
    }
}

After all permutations have been examined, we output the value of $maxGreatness.

say $maxGreatness;

(Full code on Github.)

The Perl version has some slight differences.

my $maxGreatness = 0;

We don't have a .permutations() equivalent. Luckily this has come up several times before so I was able to reuse code from previous challenges.

my @permutations;
permute { push @permutations, \@_; } @nums;

for my $perm (@permutations) {
    my $greatness = 0;

There is no Z either so I just went through all the indices from 0 to the end of the permutation (which is the same number as the end of @nums so I could have used that instead.)

    for my $i (0 .. scalar @{$perm} - 1) {
        if ($nums[$i] < $perm->[$i]) {
            $greatness++;
        }
    }

    if ($greatness > $maxGreatness) {
        $maxGreatness = $greatness;
    }
}

say $maxGreatness;

(Full code on Github.)