Perl Weekly Challenge: Week 100

Challenge 1:

Fun Time

You are given a time (12 hour / 24 hour).

Write a script to convert the given time from 12 hour format to 24 hour format and vice versa.

Ideally we expect a one-liner.

Example 1
Input: 05:15 pm or 05:15pm
Output: 17:15
Example 2
Input: 19:15
Output: 07:15 pm or 07:15pm

The spec asks for a one-liner so here goes...

($h,$m,$a)=shift=~/\A\s*((?:2[0-4])|(?:1\d)|(?:0*\d))\:([0-5]*\d)\s*([ap]m)?\s*/i;($h==0)?($h=12and$a=q{am}):($h==12)?($a=($a)?$a:q{pm}):($h>12)?($h-=12and$a=q{pm}):($a&&$a=~/pm/i)?($h+=12and$a=q{}):($a=q{});printf qq{%02d:%02d %s\n},$h,$m,$a;

(Full code on Github.)

Oof! At approximately 250 characters of inscrutable line noise, this is the kind of code that gives Perl a bad name. But if you format it and expand it out a little bit it's not that bad. In particular there are three devices, I used that are not needed in an expanded version. The use of ? ... : to replace if/else. The use of 'and' to join two statements into one. And the use of q and qq for quoting strings.

($h, $m, $ampm) = $ARGV[0] =~ / \A \s* ( (?: 2[0-4]) | (?: 1\d) | (?: 0*\d) ) \: ([0-5]*\d) \s* ([ap]m)? \s* /ix;

We take the time as a command line argument. A regular expression parses it into three parts. First there is possibly some whitespace and then the hour from 0 (or 00) to 24. The hour is followed by a colon and then minutes from 0 (or 00) to 59. After some more optional whitespace there can optionally be the string 'am' or 'pm' (or 'AM', 'PM' etc. the /i flag makes the regexp case-insensitive) and then some optional trailing whitespace. These three bits are stored as the variables $h, $m and $a (or $ampm in the expanded version for clarity.)

There are four possible scenarios on how to parse the time each dependent on the value of $hour.

if ($h == 0) {
    $h = 12;
    $ampm = 'am';

If the hour is 0 or 00, the canonical time is 12am.

} elsif ($h == 12) {
    $ampm = (defined $ampm) ? $ampm: 'pm';

If the hour is 12 it is noon so it should be 12pm. However it is possible some might write 12am refering to midnight so in order to accomodate parsing that, if $ampm was present in the input, it is left as is.

} elsif ($h > 12) {
    $h -= 12;
    $ampm = 'pm';

If the hour is greater than 12, it is 'pm' in the 24-hour format so 12 is subtracted to get the canonical hour and $ampm is set to 'pm'.

} elsif (defined $ampm && $ampm =~ /pm/i ) {
    $h += 12;
    $ampm = q{};

If the hour is less than 12 it is either 'am' in the 24-hour format or either 'am' or 'pm' in the 12-hour format. We'll only know if $ampm has been defined. If it has and it is 'pm', we add 12 to $hour to get the 24-hour format value. According to the spec we don't need to print $ampm in this case so it is cleared.

} else {
    $ampm = q{};

If $ampm wasn't 'pm' it must have been 'am'. Once again we don't need to print it in this case.

printf "%02d:%02d %s\n", $h, $m, $a;

Finally we have the time in its' canonical format so we can print it.

This is the Raku version. Raku is a little less forgiving in parsing so we need a bit more white space around certain keywords but other than that, it is the same as Perl.

my ($h,$m,$a) = @*ARGS[0].match(/^\s*(2<[0..4]>||1\d||0*\d)\:(<[0..5]>*\d)\s*(<[ap]>m)?\s*/,:i).list;($h==0)??($h=12 and $a=q{am})!!($h==12)??($a=($a)??$a!!q{pm})!!($h>12)??($h-=12 and $a=q{pm})!!($a&&$a~~m:i/pm/)??($h+=12 and $a=q{})!!($a=q{});printf qq{%02d:%02d %s\n},$h,$m,$a;

(Full code on Github.)

Challenge 2:

Triangle Sum

You are given triangle array.

Write a script to find the minimum path sum from top to bottom.

When you are on index i on the current row then you may move to either index i or index i + 1 on the next row.

Example 1
Input: Triangle = [ [1], [2,4], [6,4,9], [5,1,7,2] ]
Output: 8

Explanation: The given triangle

        2 4
       6 4 9
      5 1 7 2

The minimum path sum from top to bottom:  1 + 2 + 4 + 1 = 8

      [2]  4
      6 [4] 9
    5  [1] 7 2
Example 2
Input: Triangle = [ [3], [3,1], [5,2,3], [4,3,1,3] ]
Output: 7

Explanation: The given triangle

        3 1
       5 2 3
      4 3 1 3

The minimum path sum from top to bottom: 3 + 1 + 2 + 1 = 7

         3  [1]
        5 [2] 3
       4 3 [1] 3

This was a comparatively easy one to solve. First, the Perl version.

my ($input) = @ARGV;

my @levels = @{ eval $input };

The input is already in the form of perl syntax. That means we can get it into our program via evaluating it as perl code with the eval() function. But don't do it the way I've done it here in a real scenario. evaling outside text which has not been thoroughly vetted is an invitation for horrible things to happen. But I think we can trust Mohammed :-)

my $count = 0;
my $i = 0;

for my $level (@levels) {
    if ($level->[$i] < ($level->[$i + 1] // 'inf')) {

For each level of the triangle, we examine to see which one is smaller either the current index from the last level (starting at 0 on the top level) or the index to its' right. This might not exist if are at the edge. If it doesn't we pretend it equals infinity which will always be greater than any other value in the level.

        $count += $level->[$i];
    } else {
        $count += $level->[$i + 1];
        $i = $i + 1;

say $count;

(Full code on Github.)

This is Raku.


As I mentioned, eval (or EVAL as Raku calls it) is very powerful and fraught with peril. So much so, that the Raku compiler won't let you use it unless you include this pragma beforehand if for no other reason than to remind readers of the code you are doing something which could be dangerous.

sub MAIN(
    Str $input #= a representation of a triangle array as nested arrays
) {
    my @levels = EVAL $input;
    my $count = 0;
    my $i = 0;

    for @levels -> @level {
        if @level[$i] < (@level[$i + 1] // ∞) {

I'm sorry but I'll never get over how cool it is that Raku lets you use the ∞ symbol.

            $count += @level[$i];
        } else {
            $count += @level[$i + 1];
            $i = $i + 1;

    say $count;

(Full code on Github.)