Perl Weekly Challenge 100
This week was a lot of fun! Challenge 1 threw an additional curve ball at us – the solution should be a “one-liner.” I did my best to fit my solution on one line; the solution itself is 163 characters long.
Task 1: Fun Time
You are given a time (12 hour / 24 hour).
Write a script to convert the given time from a 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
Solution
See below for explanation and any implementation-specific comments.
sub challenge(Str \t) returns Str {
t~~/(\d+)\:(\d+)\s?([a|p]m)?/;my (\h,\m,\q)=$/[*];sprintf('%02d:%02d%s',q??h==12??q eq'am'??0!!h!!h+(12*(q eq'pm'))!!h==0|12??12!!h%12,m,q??''!!h>=12??'pm'!!'am');
}
# Implementation comments will go in this version of the above solution
sub challenge-expanded(Str \t) returns Str {
t ~~ /
(\d+) # One or more digits (should technically use \d ** {2}, but this is shorter
\: # A literal colon character
(\d+) # One or more digits (again, should use \d ** {2})
\s? # An optional space (to support HH:MMam or HH:MM am)
([a|p]m)? # An optional 'am' or 'pm' (to support both 12- and 24-hour time)
/;
my (\h, \m, \q) = $/[*]; # [1][2][3]
# The logic in here is the same as above, with added parentheses for clarity
sprintf(
'%02d:%02d%s', # [4]
q ??
(h == 12 ??
(q eq 'am' ?? 0 !! h) !!
h + (12 * ( q eq 'pm'))) !!
h == 0|12 ?? 12 !! h % 12,
m,
q ?? '' !! (h >= 12 ?? 'pm' !! 'am')
);
}
sub MAIN(Str $time) {
say challenge($time);
}
This program runs as such:
$ raku ch-1.raku 05:15 pm
17:15
$ raku ch-1.raku 19:15
7:15pm
Explanation
This one is ugly, so I apologize in advance! When I hear “one-liner” I immediately think “code golf”. I used every trick I know to make my solution as short as possible while handling all the edge cases (it’s pretty easy to handle the given test cases, but the boundaries make things tricky. I tested every possible time in my full solution on GitHub). You’ll notice I heavily lean on the ternary operator for all my branching logic.
For what it’s worth, this still has some flaws (for example, it will accept the time 99:99am
), but it accepts all valid input, so that is good enough for me.
First, we look for a string matching the regex provided (see embedded comment on what we are looking for). From this regex, we extract 3 elements: the hour, the minute, and the qualifier (am/pm) if it exists. Once we have those 3 elements, we pass them to the sprintf
function for all the logic.
For the hour, we follow the following logic:
- Is there a qualifier?
- If yes:
- Is the hour equal to 12?
- If yes:
- Is the qualifier equal to
am
?- If yes:
hour = 12
- If no:
hour
is left alone
- If yes:
- Is the qualifier equal to
- If no:
- Is the qualifier equal to
pm
?- If yes:
hour = 12 + hour
- If no:
hour
is left alone
- If yes:
- Is the qualifier equal to
- If yes:
- Is the hour equal to 12?
- If no:
- Is the hour equal to 0 or 12?
- If yes:
hour = 12
- If no:
hour = hour % 12
- If yes:
- Is the hour equal to 0 or 12?
- If yes:
Minute will always be 0-59
, so we leave it alone.
For the qualifier, we follow the following logic:
- Is there a qualifier?
- If yes, we are converting to a 24-hour format, so the new qualifier is empty
- If no:
- Is the hour greater than or equal to 12?
- If yes:
qualifier = 'pm'
- If no:
qualifier = 'am'
- If yes:
- Is the hour greater than or equal to 12?
Finally, sprintf
handles all the formatting (discussed below).
Specific comments
- Everywhere where I used a variable, you’ll notice I use
\variable-name
. In Raku, there are several sigils:$
for scalars,@
for positionals,%
for associatives, and&
for functions. There is also the special\
sigil for sigilless scalars. Basically, if a variable is defined as\variable-name
, we are able to reference it asvariable-name
. This saved me 11 characters, by my count. - A match object (returned by the smartmatch operator [
~~
]) creates a variable names$/
, so that is where that came from. I could just have easily saidmy $match = t ~~ <the rest>
, but that would cost my characters. - We used regex capturing to pull out the hour, minute, and qualifier. Those end up in the match object (
$/
) ashour = $/[0]
,minute = $/[1]
, andqualifier = $/[2]
. We are able to extract all 3 elements by using the special*
index to reference all elements in the array. - Raku’s
sprintf
function is similar to Unix’s. It takes a formatting string ('%02d:%02d%s'
) that describes the output. In this case, we say we want a 2-digit number, then a colon, then another 2-digit number, then a string. Those three elements are filled in with arguments 2-4 (hour, minute, qualifier).
Task 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
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
Solution
See below for explanation and any implementation-specific comments.
sub challenge(@triangle) {
my @layers = (0..@triangle.end); # [1]
my @indices = gather { # [2]
for @triangle -> @layer {
take (0..@layer.end).List;
}
}
my @paths = gather {
for ([X] @indices) -> @path { # [3]
my @zipped = @path Z @path[1..*]; # [4]
my $valid = True;
for @zipped -> ($a, $b) {
if $b < $a || $b > $a + 1 {
$valid = False;
last;
}
}
take @path if $valid; # [5]
}
}
my @sums = gather {
my $sum = 0;
for @paths -> @path {
for @layers Z @path -> ($layer, $index) {
$sum += @triangle[$layer][$index];
}
take $sum;
$sum = 0;
}
}
@sums.min;
}
sub MAIN(*@N where all(@N) ~~ Int) {
# Some extra logic to turn a list into a triangle
my ($index, $size) = (0, 1);
my @triangle;
while $index <= @N.end {
my $end-index = $index + $size;
my @layer = @N[$index..^$end-index];
@triangle.push(@layer);
$index = $end-index;
$size++;
}
say challenge(@triangle);
}
This program runs as such:
$ raku ch-2.raku 1 2 4 6 4 9 5 1 7 2
8
Explanation
The logic here is pretty straightforward:
- Find how many layers to the triangle there are
- Find the valid indices of each layer. So, for example 1, this would be something like
((0), (0, 1), (0, 1, 2), (0, 1, 2, 3))
- Find all valid paths. “Valid” in this case means that we always move from position
i
to positioni
ori+1
on the next layer. - Find the sum of each valid path.
- Return the minimum sum out of the valid paths.
Specific Comments
- Raku has a great method for positionals called
end
. It returns the last index in a list and saves us from confusion (similar to something likelen(list) - 1
). gather
is a way to build up a list based on some logic. It can be thought of as a more powerful list comprehension (from Python).X
is the cross product operator. When used like[X] @list
, it works like this:[X] ((1, 2, 3), (4, 5, 6), (7, 8, 9)) == (1, 2, 3) X (4, 5, 6) X (7, 8, 9)
. In this case, it creates all possible paths through the triangle (which we filter down to valid paths).- To make sure we only move from position
i
to positioni+1
from layer to layer, we “zip” against our path from positioni+1
to the end. if
can be used in a postfix form to save space. In this case, we only want to take a path if it is valid (as defined above).
Final Thoughts
I had a lot of fun with this week’s challenges, especially challenge 1! Let me know if you think of a shorter solution. Otherwise, see y’all next week!