Perl Weekly Challenge 206: hard times!
This post presents my solutions to the Perl Weekly Challenge 206.I keep doing the Perl Weekly Challenge in order to mantain my coding skills in good shape, as well as in order to learn new things, with particular regard to Raku, a language that I love.
This week, I solved the following tasks:
- PWC 206 - Task 1 - Raku
- PWC 206 - Task 2 - Raku
- PWC 206 - Task 1 in PostgreSQL PL/Perl
- PWC 206 - Task 2 in PostgreSQL PL/Perl
- PWC 206 - Task 1 in PostgreSQL PL/PgSQL
- PWC 206 - Task 2 in PostgreSQL PL/PgSQL
Raku Implementations
PWC 206 - Task 1 - Raku Implementation
The first task was about computing the lowest difference between an array of times, expressed asHH::MI
strings.
At first, I thought at DateTime
but unluckily I was on a machine were the Raku documentation was absent and the online manual https://docs.raku.org
was suffering too.
So, I had to implement it on my own.
sub diff ( $start, $end ) {
my ( $start-hours, $start-mins ) = $start.chomp.split( ':' );
my ( $end-hours, $end-mins ) = $end.chomp.split( ':' );
if ( $start-hours == 0 ) {
$start-hours = 23;
$start-mins += 60;
}
if ( $end-hours == 0 ) {
$end-hours = 23;
$end-mins += 60;
}
my $diff-hours = abs( $end-hours - $start-hours );
my $diff-mins = abs( $end-mins - $start-mins ) % 60;
return $diff-hours * 60 + $diff-mins;
}
sub MAIN( :$verbose = True, *@times where { @times.grep( * ~~ / ^ \d ** 2 ':' \d ** 2 $ / ).elems == @times.elems } ) {
my %diffs;
%diffs{ diff( $_[ 1 ], $_[ 0 ] ) } = [ $_[0], $_[1] ] for @times.sort.combinations( 2 );
%diffs.keys.map( *.Int ).min.say;
%diffs{ %diffs.keys.map( *.Int ).min }.join( ' - ' ).say if ( $verbose );
}
The function
diff
accepts two time strings, computes the numeric values for hours and minutes and then returns the difference expressed as the number of minutes. There is a little trick in there: if the hour is expressed as 00:MI
I downgrade it to 23
and add sixty minutes. This allows me to compare every time without having to worry about the round clock.
In the
MAIN
I create a %diffs
hash keyed by the difference in minutes between every pair (combinations(2)
) after the list has been sorted
, so that I can consider one way difference. Last, I simply print the minimium key (as an integer, because it is managed as a string).
PWC 206 - Task 2 - Raku Implementation
Given an array of integers, consider all the pairs and sum the minimum of every pair, then get the overall minimum sum.sub MAIN( *@list where { @list.elems %% 2 && @list.grep( * ~~ Int ).elems == @list.elems } ) {
my @sums;
for @list.permutations {
for $_.rotor( 2 ) -> $a, $b {
@sums.push: sum( min( $a ) + min( $b ) );
}
}
@sums.min.say;
}
The above is a very unefficient solution, since it exploits all the
permutations
of the input array and consumes its as slices of two elements. When I was developing the PL/Perl solution a trick came into my mind.
PL/Perl Implementations
A similar implementation to the Raku one: I use an anonymous subroutine to compute the difference between time strings.PWC 206 - Task 1 - PL/Perl Implementation
CREATE OR REPLACE FUNCTION
pwc206.task1_plperl( text[] )
RETURNS int
AS $CODE$
my ( $times ) = @_;
my $computations = {};
my $min;
my $diff = sub {
my ( $start, $end ) = @_;
$start =~ /^(\d{2}):(\d{2})$/;
my ( $start_hours, $start_mins ) = ( $1, $2 );
$end =~ /^(\d{2}):(\d{2})$/;
my ( $end_hours, $end_mins ) = ( $1, $2 );
if ( $start_hours == 0) {
$start_hours = 23;
$start_mins += 60;
}
if ( $end_hours == 0) {
$end_hours = 23;
$end_mins += 60;
}
return abs( $end_hours - $start_hours ) * 60 + abs( $end_mins - $start_mins ) % 60;
};
for my $begin ( sort $times->@* ) {
for my $end ( sort $times->@* ) {
next if ( $begin eq $end );
my $difference = $diff->( $end, $begin );
$computations->{ $difference } = [ $begin, $end ];
$min = $difference if ( ! $min || $difference < $min );
}
}
elog(INFO, "Min is $min minutes from " . join( ',', $computations->{ $min }->@* ) );
return $min;
$CODE$
LANGUAGE plperl;
PWC 206 - Task 2 - PL/Perl Implementation
Here I had an idea: if I sort the list of integers and then take one element out of two, I can sum all the mimum values of every pair and get, in a straight manner, the overall minimu sum.CREATE OR REPLACE FUNCTION
pwc206.task2_plperl( int[] )
RETURNS int
AS $CODE$
my ( $list ) = sort $_[ 0 ];
my $sum = 0;
while ( $list->@* ) {
$sum += shift $list->@*;
shift $list->@*;
}
return $sum;
$CODE$
LANGUAGE plperl;
PostgreSQL Implementations
PWC 206 - Task 1 - PL/PgSQL Implementation
Luckily, a difference between twotime
values is another time
value, therefore it handles the difference right.
CREATE OR REPLACE FUNCTION
pwc206.task1_plpgsql( t text[] )
RETURNS time
AS $CODE$
DECLARE
m time;
t1 text;
t2 text;
BEGIN
FOREACH t1 IN ARRAY t LOOP
FOREACH t2 IN ARRAY t LOOP
IF t1 = t2 THEN
CONTINUE;
END IF;
IF m IS NULL OR ( t2::time - t1::time ) < m THEN
m := ( t2::time - t1::time );
END IF;
END LOOP;
END LOOP;
RETURN m;
END
$CODE$
LANGUAGE plpgsql;
PWC 206 - Task 2 - PL/PgSQL Implementation
A single query to do the same as the PL/Perl implementation. I build a temporary table, using a CTE, where every row has a number associated to it by means ofrow_number()
. Then I select all the odd rows, that represent the minimum value of the sorted input array, and the sum is the overall minimum.
CREATE OR REPLACE FUNCTION
pwc206.task2_plpgsql( l int[] )
RETURNS int
AS $CODE$
DECLARE
res int;
BEGIN
WITH data AS (
SELECT v, row_number() OVER ( ORDER BY v ) r
FROM unnest( l ) v
)
SELECT sum( v )
INTO res
FROM data
WHERE r % 2 <> 0
;
RETURN res;
END
$CODE$
LANGUAGE plpgsql;