Perl Weekly Challenge: Week 347

Challenge 1:

Format Date

You are given a date in the form: 10th Nov 2025.

Write a script to format the given date in the form: 2025-11-10 using the set below.

@DAYS   = ("1st", "2nd", "3rd", ....., "30th", "31st")
@MONTHS = ("Jan", "Feb", "Mar", ....., "Nov",  "Dec")
@YEARS  = (1900..2100)
Example 1
Input: $str = "1st Jan 2025"
Output: "2025-01-01"
Example 2
Input: $str = "22nd Feb 2025"
Output: "2025-02-22"
Example 3
Input: $str = "15th Apr 2025"
Output: "2025-04-15"
Example 4
Input: $str = "23rd Oct 2025"
Output: "2025-10-23"
Example 5
Input: $str = "31st Dec 2025"
Output: "2025-12-31"

Perl first for a change. As I've mentioned before, in date or time related challenges it is alwats prudent to use CPAN modules containing tried and tested code rather than rolling your own and falling afoul of the many oddities and edge cases in this area of programming. This time (haha) I'm also using a module—Time::Piece. This has the added benefit of being included in standard distributions of Perl.

After importing it like this:

use Time::Piece;

We can use the strptime() constructor to create a Time::Piece object and then use its' ymd() method to output the date in the specs' desired format (i.e. YYYY-MM-DD).

say Time::Piece->strptime($str, '%d %b %Y')->ymd;

strptime() is based on the POSIX C library strptime(3) function though as far as I know, Time::Piece implements the API itself for portabilities sake rather than call the C function directly. It takes two parameters; the first is the date to be parsed (i.e. $str) and the second is a string describing the format of the date. And that makes it flexible enough for most date parsing purposes but there is a problem. The %d format specifier for days only takes the day number into parsing consideration not its' ordinal suffix. (I.e. 1 not 1st, 2 not 2nd and so on.) So before running the line above, we have to preprocess $str and remove those suffixes.

$str =~ s/st|nd|rd|th//;

(Full code on Github.)

Raku has a standard Date class but to my suprise, it is missing anything equivalent to strptime(). It seems there is a DateTime::Format(https://raku.land/zef:raku-community-modules/DateTime::Format) module that provides this functionality but I was unable to install it with zef. Rather than spend time debugging why that was, I just pressed on and wrote my own code despite my own advice not to.

I created a map between month abbreviations and (zero-padded) month numbers.

my %months = (
    Jan => '01',
    Feb => '02',
    Mar => '03',
    Apr => '04',
    May => '05',
    Jun => '06',
    Jul => '07',
    Aug => '08',
    Sep => '09',
    Oct => '10',
    Nov => '11',
    Dec => '12'
);

Then we preprocess $str to remove ordinal suffixes just as in Perl. We have to use S/// and assign to a different variable because $str is immutable.

my $date = S:g/(st|nd|rd|th)// given $str;

Then the three parts of the $date are split from it with .words() and assigned to variables.

my ($day, $month, $year) = $date.words;

These three variables (with $month mapped to the appropriate month number) are used to construct a Date object. Lucky for us, the standard representation of this object is in the exact format we need so we can just output it with .say().

Date.new($year, %months{$month}, $day).say;

(Full code on Github.)

Challenge 2:

Format Phone Number

You are given a phone number as a string containing digits, space and dash only.

Write a script to format the given phone number using the below rules:

1. Removing all spaces and dashes
2. Grouping digits into blocks of length 3 from left to right
3. Handling the final digits (4 or fewer) specially:
   - 2 digits: one block of length 2
   - 3 digits: one block of length 3
   - 4 digits: two blocks of length 2
4. Joining all blocks with dashes
Example 1
Input: $phone = "1-23-45-6"
Output: "123-456"
Example 2
Input: $phone = "1234"
Output: "12-34"
Example 3
Input: $phone = "12 345-6789"
Output: "123-456-789"
Example 4
Input: $phone = "123 4567"
Output: "123-45-67"
Example 5
Input: $phone = "123 456-78"
Output: "123-456-78"

We begin by implementing rule 1 from the spec and removing spaces and dashes. As in the Raku solution for challenge 1, we have to account for the input being immutable.

my $formatted = S:g/' ' || \-// given $phone;

The next line implements rule 2.

my @groups = $formatted.comb(3);

I did not literally follow rule 3 because it seems to me that given the way we have created groups of three, the only remaining case to handle is if the last group only contains 1 digit. (If it contained 2 or 3, it would already be in its' final form and a 4 digit group cannot occur the way we have done grouping.)

So we check to see if the last group has a length of 1. For the sake of truth, I should admit that first I made a silly mistake here and used .elems() which is for array length instead of .chars() which is for string length and was baffled for a while as to why my groups were coming out the wrong length.

Anyway if the last group is indeed onlu one digit...

if @groups[*-1].chars == 1 {

...we capture the last digit of the second-to-last group for later use and erase it.

    @groups[*-2] = @groups[*-2].subst(/(.)$/, q{});

Then we append the captured digit (in $0) to the beginning of the last group.

    @groups[*-1] = "$0@groups[*-1]";

Now the last group has a length of 2 as does the second-to-last group thereby fulfiling rule 3.

}

Finally we implement rule 4 and print the result.

@groups.join(q{-}).say;

(Full code on Github.)

This is the Perl version which is mostly the same as Raku except we don't have to deal with all that immutability business.

$phone =~ s/[ \-]//g;
my @groups = $phone =~ /.{1,3}/g;

if (length $groups[-1] == 1) {
    $groups[-2] =~ s/(.)$//;

And the first capture group is $1 not $0 which always mixes me up now that I use both Perl and Raku.

    $groups[-1] = "$1$groups[-1]";
}

say join q{-}, @groups;

(Full code on Github.)