This is my response to the Perl Weekly Challenge #100.
Input: 05:15 pm or 05:15pm
Output: 17:15
Example 2:
Input: 19:15
Output: 07:15 pm or 07:15pm
The program should support AM values as well, even if they are missing from the examples.
The conversion can be summarised like this:
24 Hour | 12 Hour |
---|---|
00:xx | 12:xx am |
01:xx | 01:xx am |
11:xx | 11:xx am |
12:xx | 12:xx pm |
13:xx | 01:xx pm |
23:xx | 11:xx pm |
24:00 | 12:00 am (next day) |
See e.g. en.wikipedia.org/wiki/24-hour_clock for more information about conversion between the 12 and 24 hour formats.
Here it is, as a one liner (if you remove the crash bang line, and the newlines):
File: fun-time
#! /usr/bin/env raku
@*ARGS[0] ~~ /^(\d\d)\:(\d\d) \s? ([<[ap]>m]?)$/ && $2.chars # [1]
## 1a ####### 1b ########### # 1c # 1d ######### # 1e ######
?? ($2 eq "pm" # [2]
?? say "{ $0 eq '12' ?? '12' !! $0 + 12 }:{ $1 }" # [2a]
!! say "{ $0 eq '12' ?? '00' !! $0 }:{ $1 }" # [2b]
)
!! (12 <= $0 < 24 # [3]
?? say "{ $0 eq '12' ?? '12' !! ($0 - 12).fmt('%02d') }:{ $1 } pm" # [3a]
!! say "{ $0 eq '00'|'24' ?? '12' !! $0 }:{ $1 } am" # [3b]
);
[1] Start with the first argument on the command line [1a], and compare it with this
regex, matching the entire string (^
…$
). First we have two
digits (the hour, ends up in $0
), a colon and two additional digits
(the minutes, ends up in $1
) [1b]. (This assumes, as the examples do,
that we cannot specify a one digit hour value without a leading zero.) This can
optionally be followed by a single space [1c], and this again by an optional "am" or
"pm" [1d]. The "am" or "pm" match end up in $2
, if found, and we check
if we got a match like this [1e].
[2] We got an am/pm value. If «am», print the hour if it is 12, if not add 12 to it [2a]. If not (it is «pm»), print 12 if the hour is 00, if not print the hour itself.
[3] We have a 24 hour value if we get here. If the hour is in the
interval 12 to 23 [3a], print the hour if it is 12 - or subtracted with 12 if not
(i.e. it is in the interval 13 to 23). Note the fmt
call to ensure two
digits (i.e. a leading zero). If it is not in this interval (i.e. less than 12) [3b],
print 12 if the hour is 00 or 24 - and the hour value otherwise.
See
docs.raku.org/routine/fmt
for more information about fmt
.
Running it:
$ ./fun-time "00:00" ## -> 12:00 am
$ ./fun-time "01:00" ## -> 01:00 am
$ ./fun-time "11:00" ## -> 11:00 am
$ ./fun-time "12:00" ## -> 12:00 pm
$ ./fun-time "13:00" ## -> 01:00 pm
$ ./fun-time "23:00" ## -> 11:00 pm
$ ./fun-time "24:00" ## -> 12:00 am
$ ./fun-time "12:00 am" ## -> 00:00
$ ./fun-time "01:00 am" ## -> 01:00
$ ./fun-time "11:00 am" ## -> 11:00
$ ./fun-time "12:00 pm" ## -> 12:00
$ ./fun-time "01:00 pm" ## -> 13:00
$ ./fun-time "11:00 pm" ## -> 23:00
Error checking is missing. Garbage in, garbage out:
$ ./fun-time "99:99 pm" ## -> 111:99
#! /usr/bin/env perl
use strict;
use warnings;
$ARGV[0] =~ /^(\d\d)\:(\d\d)\s?(am|pm)?$/ && length($3)
? ($3 eq "pm"
? print $1 eq '12' ? '12' : $1 + 12, ":$2\n"
: print $1 eq '12' ? '00' : $1, ":$2\n"
)
: ($1 >= 12 && $1 < 24
? print $1 eq '12' ? '12' : sprintf('%02d', $1 - 12), ":$2 pm\n"
: print $1 eq '00'|| $1 eq '24' ? '12' : $1, ":$2 am\n"
);
Running it gives the same result as the Raku version:
$ ./fun-time-perl "00:00" ## -> 12:00 am
$ ./fun-time-perl "01:00" ## -> 01:00 am
$ ./fun-time-perl "11:00" ## -> 11:00 am
$ ./fun-time-perl "12:00" ## -> 12:00 pm
$ ./fun-time-perl "13:00" ## -> 01:00 pm
$ ./fun-time-perl "23:00" ## -> 11:00 pm
$ ./fun-time-perl "24:00" ## -> 12:00 am
$ ./fun-time-perl "12:00 am" ## -> 00:00
$ ./fun-time-perl "01:00 am" ## -> 01:00
$ ./fun-time-perl "11:00 am" ## -> 11:00
$ ./fun-time-perl "12:00 pm" ## -> 12:00
$ ./fun-time-perl "01:00 pm" ## -> 13:00
$ ./fun-time-perl "11:00 pm" ## -> 23:00
Error checking is missing here as well:
$ ./fun-time-perl "99:99 pm" ## -> 111:99
i
on the current row then you may move to either
index i
or index i + 1
on the next row.
Input: Triangle = [ [1], [2,4], [6,4,9], [5,1,7,2] ]
Output: 8
Explanation: The given triangle
1
2 4
6 4 9
5 1 7 2
The minimum path sum from top to bottom: 1 + 2 + 4 + 1 = 8
[1]
[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
3 1
5 2 3
4 3 1 3
The minimum path sum from top to bottom: 3 + 1 + 2 + 1 = 7
[3]
3 [1]
5 [2] 3
4 3 [1] 3
We have traversed triangles several times before. The following solution is inspired by my «sum-path» program from Pointy Path with Raku. (Scroll down to «Challenge #093.2: Sum Path»).
Note that the challenge only asks for the result, not the actual path. So we can get away with this simple program:
File: triangle-sum
#! /usr/bin/env raku
unit sub MAIN (Str $tree = "1 | 2 4 | 6 4 9 | 5 1 7 2"); # [1]
my @tree = $tree.split("|")>>.words; # [2]
my @solutions; # [3]
traverse(0, 0, 0); # [4]
sub traverse($row, $col, $sum is copy) # [4a]
{
$sum += @tree[$row][$col]; # [5]
if @tree[$row+1] # [6]
{
traverse($row+1, $col, $sum); # [7]
traverse($row+1, $col+1, $sum); # [7a]
}
else # [8]
{
@solutions.push: $sum; # [8a]
}
}
say @solutions.min; # [9]
[1] The default triangle. Note the |
to separate the rows.
[2]
Split the string into a two dimentional array. The split
takes care of the first part (the rows), and .>>words
splits the
rows into separate values. The .>>
part tells Raku to apply the
function (on the right) on each item in the list (to the left).
[3] We are storing the solutions (the sum of each possible path) here.
[4] Start at row 0, column 0 and with the accumulated sum (not including the current position) 0. Off we go, recursively
[5] Add the value of the current position to the sum.
[6] Do we have another row (below the current one)?
[7] • follow the left [7] and right [7a] children.
[8] No children? Then we are done, and can add the sum to the list of solutions.
[9] Get the lowest value (in the list) with min
,
and print it.
See
docs.raku.org/routine/split
for more information about split
.
See
docs.raku.org/routine/words
for more information about words
.
See
docs.raku.org/routine/min
for more information about min
.
Running it:
$ ./triangle-sum "1 | 2 4 | 6 4 9 | 5 1 7 2"
8
$ ./triangle-sum "3 | 3 1 | 5 2 3 | 4 3 1 3"
7
We certainly got the correct result, but it would have been nice to see why.
That requires quite a lot of extra code:
File: triangle-sum-verbose
#! /usr/bin/env raku
unit sub MAIN (Str $tree = "1 | 2 4 | 6 4 9 | 5 1 7 2",
:v(:$verbose),
:c(:$colour),
:h(:$html-colour));
class TrianglePath # [1]
{
has Int $.sum; # [1a]
has @.path; # [1b]
}
constant ansi-blue = "\e[44m"; # [7]
constant ansi-green = "\e[42m";
constant ansi-red = "\e[101m";
constant ansi-stop = "\e[0m";
constant html-blue = '<span class="text-light bg-primary">';
constant html-green = '<span class="text-light bg-success">';
constant html-red = '<span class="text-light bg-danger">';
constant html-stop = '</span>';
my @tree = $tree.split("|")>>.words;
my $width = $tree.split("|")>>.words>>.chars>>.max.max; # [2]
my $height = @tree.elems; # [2a]
my @solutions;
traverse(0, 0, 0, ()); # [3]
sub traverse($row, $col, $sum is copy, @path is copy) # [3a]
{
@path.push: $col;
$sum += @tree[$row][$col];
if @tree[$row+1]
{
traverse($row+1, $col, $sum, @path);
traverse($row+1, $col+1, $sum, @path);
}
else
{
say ": Sum: $sum. Path (as indices on the rows): { @path.raku }"
if $verbose;
@solutions.push: TrianglePath.new(sum => $sum, path => @path); # [4]
}
}
my $first = @solutions.sort({$^a.sum <=> $^b.sum }).first; # [5]
if $colour | $html-colour
{
my $start = $html-colour ?? html-green !! ansi-green;
my $stop = $html-colour ?? html-stop !! ansi-stop;
for ^@tree.elems -> $row # [6]
{
say ": "
~ (" " x $width * ($height - $row -1)) ~ (^@tree[$row].elems)
.map({ $_ eq $first.path[$row]
?? $start ~ @tree[$row][$_].fmt("%{$width}d") ~ $stop
!! (@tree[$row][$_].fmt("%{$width}d")) }
).join(" " x $width);
}
}
say $first.sum;
[1] We are going to print out the best triangle, so we need a way of keeping track as we try them all. THis class does that, with the sum [1a] and path (as indices on each row, from top to bottom) [1b].
[2] We need these when we print the triangle. The width is the maximum number of characters used on the values [2], and the height is the number of rows [2a].
[3] We pass along the path as well, this time. Note the is copy
, so that
we end up with a copy that we can add to locally (so that the left and right recursion
do not interfere with each other) [3a].
[4] Generate the object when we have reached the bottom row.
[5] We have a list of paths (TrianglePath objects). Sort it by the
sum field, and get the first one (with first
. That is the one with the
lowest sum. (Note that we can have several paths with the same sum, but that does not
really matter.)
[6] Print the triangle, with the path highlighted. See if you can understand the code…
[7] I have included code for three colours here, even if we only use green.
Running it with verbose mode, to see the different paths and their values:
$ ./triangle-sum-verbose -v "1 | 2 4 | 6 4 9 | 5 1 7 2"
: Sum: 14. Path (as indices on the rows): [0, 0, 0, 0]
: Sum: 10. Path (as indices on the rows): [0, 0, 0, 1]
: Sum: 8. Path (as indices on the rows): [0, 0, 1, 1]
: Sum: 14. Path (as indices on the rows): [0, 0, 1, 2]
: Sum: 10. Path (as indices on the rows): [0, 1, 1, 1]
: Sum: 16. Path (as indices on the rows): [0, 1, 1, 2]
: Sum: 21. Path (as indices on the rows): [0, 1, 2, 2]
: Sum: 16. Path (as indices on the rows): [0, 1, 2, 3]
8
$ ./triangle-sum-verbose -v "3 | 3 1 | 5 2 3 | 4 3 1 3"
: Sum: 15. Path (as indices on the rows): [0, 0, 0, 0]
: Sum: 14. Path (as indices on the rows): [0, 0, 0, 1]
: Sum: 11. Path (as indices on the rows): [0, 0, 1, 1]
: Sum: 9. Path (as indices on the rows): [0, 0, 1, 2]
: Sum: 9. Path (as indices on the rows): [0, 1, 1, 1]
: Sum: 7. Path (as indices on the rows): [0, 1, 1, 2]
: Sum: 8. Path (as indices on the rows): [0, 1, 2, 2]
: Sum: 10. Path (as indices on the rows): [0, 1, 2, 3]
7
We can show the tree that gives us the lowest sum, with the path highlighted:
$ ./triangle-sum-verbose -c "1 | 2 4 | 6 4 9 | 5 1 7 2"
: 1
: 2 4
: 6 4 9
: 5 1 7 2
8
$ ./triangle-sum-verbose -c "3 | 3 1 | 5 2 3 | 4 3 1 3"
: 3
: 3 1
: 5 2 3
: 4 3 1 3
7
Use the «-h» command line option instead, if you want output suitable for inclusion in an html document (using Bootstrap colours).
Looking food.
#! /usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use feature 'signatures';
use String::Util 'trim';
use List::Util 'min';
no warnings "experimental::signatures";
my $tree = shift(@ARGV) // "1 | 2 4 | 6 4 9 | 5 1 7 2";
my @tree;
for my $row (split('\|', $tree)) # [1]
{
my @row = split(/\s+/, trim($row)); # [1a]
push(@tree, \@row); # [1b]
}
my @solutions;
traverse(0, 0, 0);
sub traverse($row, $col, $sum)
{
$sum += $tree[$row][$col];
if ($tree[$row+1])
{
traverse($row+1, $col, $sum);
traverse($row+1, $col+1, $sum);
}
else
{
push(@solutions, $sum);
}
}
say min(@solutions);
[1] Generating a two dimentional array in Perl is harder than in Raku. We start by
splitting on the |
to get the lines [1]. (Note that we have to quote
the bar character.) Then we get the values for each row [1a], and add those rows to
the array. Note the \
character telling Perl to use a pointer to the
variable. Skipping that would have given us a single one dimentional array.
Running it gives the same result as the Raku version:
$ ./triangle-sum-perl "1 | 2 4 | 6 4 9 | 5 1 7 2"
8
$ ./triangle-sum-perl "3 | 3 1 | 5 2 3 | 4 3 1 3"
7
And that's it.