Perl Weekly Challenge: Week 296
Challenge 1:
String Compression
You are given a string of alphabetic characters,
$chars
.Write a script to compress the string with run-length encoding, as shown in the examples.
A compressed unit can be either a single character or a count followed by a character.
BONUS: Write a decompression function.
Example 1
Input: $chars = "abbc"
Output: "a2bc"
Example 2
Input: $chars = "aaabccc"
Output: "3ab3c"
Example 3
Input: $chars = "abcc"
Output: "ab2c"
One of the great features of Raku is multi-dispatch. We can define two or more subroutines with the same name and different function signatures and Raku will use the version that matches best to the parameters given. This allows us to organize our code very cleanly as we shall see.
This function RLE encodes a string. It takes the mandatory command-line switch -e
followed by the string to
encode (which will be stored in $e
).
multi sub MAIN(
Str :$e! #= run-length encode a string
) {
I thought sbout breaking up the string into a list of characters and iterating through it encoding
runs, but then I decided it is simple enough to do with regular expressions and the s///
operator.
A problem is that function parameters in Raku are immutable and s///
alters the strings it operates on by
making substitutions. Luckily there is a workaround; Raku has a S///
operator that works on a copy of the string
specified by given
.
The first half of s///
will match and capture a character (labeled $0
) followed by 1 or more of the same character which will also be captured as a group which from the parentheses will also be labeled $0
(and the old $0
from that perspective will be $0[0]
). This is a bit confusing until you get it.
In the second half, $0
is replaced (in the copy string) with the length of $0
(found with .chars()
) and $0[0]
. The :g
flag to S///
performs this operation globally on all runs found in the string. Finally say()
prints
out the encoded string.
say S:g/((.)$0+)/$($0.chars)$0[0]/ given $e;
}
The bonus task was to create a decode function. This is done by a variant of MAIN()
which takes the -d
switch instead of-e
.
multi sub MAIN(
Str :$d!, #= decode a run-length encoded string
) {
We also use S///
here but this time, we look for a number followed by a character and replace it with that
number of the character using the x
operator.
say S:g/(\d+)(.)/$($1 x $0)/ given $d;
}
Perls' command-line processing is not as sophisticated as Raku. I chose to use the Getopt::Std module to cover the gaps.
The getopts()
function from that module that specifies that this script takes two command-line switches, -d
and
-e
each of which takes a string argument. We declare two package variables $opt_d
and $opt_e
two hold those
strings.
our($opt_d, $opt_e);
getopts('d:e:');
Just as in Raku, we use regular expressions and the s///
operator (no need to worry about immutability.)
if (defined $opt_d) {
$opt_d =~ s/(\d+)(.)/$2 x $1/ge;
say $opt_d;
} elsif (defined $opt_e) {
$opt_e =~ s/((.)\g2+)/(length $1) . $2/ge;
say $opt_e;
If neither -d
or -e
was specified, we call our own usage()
function and exit. Raku takes care
of this kind of thing for us.
} else {
usage();
}
Challenge 2:
Matchsick Square
You are given an array of integers,
@ints
.Write a script to find if it is possible to make one square using the sticks as in the given array
@ints
where$ints[ì]
is the length ofi
th stick.
Example 1
Input: @ints = (1, 2, 2, 2, 1)
Output: true
Top: $ints[1] = 2
Bottom: $ints[2] = 2
Left: $ints[3] = 2
Right: $ints[0] and $ints[4] = 2
Example 2
Input: @ints = (2, 2, 2, 4)
Output: false
Example 3
Input: @ints = (2, 2, 2, 2, 4)
Output: false
Example 4
Input: @ints = (3, 4, 1, 4, 3, 1)
Output: true
Perl first for a change.
Think about this, a square has 4 sides of equal length. Therefore the sum of all the matchsticks must
be evenly divisible by 4 and no matchstick can be longer than the length of a side. We can express that
in Perl like this (assuming the matchsticks are input as command-line parameters.) We have to provide our own
max()
and sum()
functions.
my $side = sum(@ARGV) / 4;
say $side == int($side) && max(@ARGV) <= $side ? 'true' : 'false';
In Raku we can squeeze this down to a one-liner.
my $a = @*ARGS.sum / 4; say $a %% 1 && @*ARGS.max <= $a