I’m not really feeling music recently. I’m not feeling much of anything, but I need to do the challenge. So here’s Perl Weekly Challenge 296.
Task 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"
Approach
This is pretty simple: make a single pass over the string, character by character, keeping track of the last character seen, how many times that character was seen, and a result string. Populate the last character with the first character in the string, make the count 1, and then proceed through the rest of the string. If the current character is the same as the last character, increment the count and move on. If the current character is different, output the count followed by the last character to the result string, with the special case that if the count is 1, only output the character. Then replace the last character with the current character, reset the count to 1, and move on. At the end of the string, output the last character/count to the result string like we did while we were processing the string.
Decompression is even easier: if we get an alphabetic character, output it to the result. If we have a run of numeric characters, interpret that as a count, and output that many of the next alphabetic character found.
Raku
For the decode function, I decided to let Raku’s .comb
routine do the heavy lifting for me. By having .comb
match a run of numeric characters or a single alphabetic character. Then the processing just becomes pulling each element off the beginning of the resulting array and checking to see whether it’s a letter or a not.
sub rlEncode($str) {
my @chars = $str.comb;
my $last = @chars.shift;
my $count = 1;
my $result;
for @chars -> $c {
if ($c eq $last) { # same as last char,
$count++; # increment the count
}
else {
if ($count > 1) { # if the count > 1, include the
$result ~= $count; # count in the encoded output
}
$result ~= $last; # add the last character to the output
$last = $c; # make current char the last
$count = 1; # and reset the count
}
}
# encode the last run of characters in the string
if ($count > 1) {
$result ~= $count;
}
$result ~= $last;
return $result;
}
sub rlDecode($str) {
my @chars = $str.comb(/<digit>+|<alpha>/);
my $result;
while (my $c = @chars.shift) {
if ($c ~~ /<alpha>/) { # it's an alphabetic character
$result ~= $c;
}
else { # it's a numeric count
my $count = $c;
$c = @chars.shift; # get the next character
$result ~= $c x $count;
}
}
return $result;
}
View the entire Raku script for this task on GitHub.
$ raku/ch-1.raku
Example 1:
Input: $chars = "abbc"
Encoded: "a2bc"
Decoded: "abbc"
Example 2:
Input: $chars = "aaabccc"
Encoded: "3ab3c"
Decoded: "aaabccc"
Example 3:
Input: $chars = "abcc"
Encoded: "ab2c"
Decoded: "abcc"
Example 4:
Input: $chars = "abbbbbbbbbbbbccccccdd"
Encoded: "a12b6c2d"
Decoded: "abbbbbbbbbbbbccccccdd"
Perl
In Perl, I didn’t have something like .comb
, so I just built the count character by character.
sub rlEncode($str) {
my @chars = split //, $str;
my $last = shift @chars;
my $count = 1;
my $result;
foreach my $c ( @chars ) {
if ($c eq $last) { # same as last char,
$count++; # increment the count
}
else {
if ($count > 1) { # if the count > 1, include the
$result .= $count; # count in the encoded output
}
$result .= $last; # add the last character to the output
$last = $c; # make current char the last
$count = 1; # and reset the count
}
}
# encode the last run of characters in the string
if ($count > 1) {
$result .= $count;
}
$result .= $last;
return $result;
}
sub rlDecode($str) {
my $result = '';
my $count = '';
foreach my $c (split //, $str) {
if ($c =~ /\D/) { # it's an alphabetic character
if ($count) { # if there's a count
$result .= $c x $count; # output that many of this char
$count = ''; # reset the count
}
else { # append character to output
$result .= $c;
}
}
else { # it's part of a numeric count
$count .= $c;
}
}
return $result;
}
View the entire Perl script for this task on GitHub.
Python
Because Python has slightly more typing than Raku or Perl, on lines 13 and 33 I had to make sure I was dealing with the count as a string and as a number to get the results I wanted.
def rlEncode(charstr):
chars = [ c for c in charstr ]
last = chars.pop(0)
count = 1
result = ''
for c in chars:
if c == last: # same as last char,
count += 1 # increment the count
else:
if count > 1: # if the count > 1, include the
result += str(count) # count in the encoded output
result += last # add the last character to the output
last = c # make current char the last
count = 1 # and reset the count
# encode the last run of characters in the string
if count > 1:
result += str(count)
result += last
return result
def rlDecode(charstr):
result = ''
count = ''
for c in charstr:
if c.isnumeric():
count += c
else:
if count: # if there's a count
result += c * int(count) # output that many of this char
count = '' # reset the count
else: # append character to output
result += c
return result
View the entire Python script for this task on GitHub.
Elixir
Because it’s functional, Elixir required adjusting the approach a little, but not much. We’re still processing the strings character by character. But I did spin out the code to append to the result string into its own function so we can call it multiple times.
def encodeLastCount(last, count, result) do
if count > 1 do
result <> to_string(count) <> last
else
result <> last
end
end
def rlEncode([], last, count, result), do:
encodeLastCount(last, count, result)
def rlEncode(, last, count, result) do
if c == last do
# increment the count of times we saw this character
rlEncode(rest, last, count + 1, result)
else
# make c the new last and 1 the new count
rlEncode(rest, c, 1, encodeLastCount(last, count, result))
end
end
def rlEncode(chars) do
chars = String.graphemes(chars) # break string into chars
[last | rest] = chars # grab the first character
rlEncode(rest, last, 1, "") # start with that as last char
end
def addCharToResult(count, char, result) do
count = if String.length(count) > 0 do
# if there's a count, convert to int
String.to_integer(count)
else
1 # empty count string means one char
end
result <> String.duplicate(char, count) # repeat the char
end
def rlDecode([], _, result), do: result
def rlDecode([next | rest], count, result) do
if Regex.match?(~r/\d/, next) do
# it's a numeric char, add it to the count
# and process the next character
rlDecode(rest, count <> next, result)
else
# it's an alpha char, add it to the result
rlDecode(rest, "", addCharToResult(count, next, result))
end
end
def rlDecode(chars) do
chars = String.graphemes(chars) # break string into chars
rlDecode(chars, "", "")
end
def solution(chars) do
IO.puts("Input: $chars = \"#{chars}\"")
encoded = rlEncode(chars)
IO.puts("Encoded: \"#{encoded}\"")
decoded = rlDecode(encoded)
IO.puts("Decoded: \"#{decoded}\"")
end
View the entire Elixir script for this task on GitHub.
Task 2: Matchstick 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 of i
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
Approach
This should just be a math problem. If we sum up the ints and divide by 4, if we don’t get an integer result, we know we can’t create a square from the integers. But even if we get an integer result, we might not be able to make a square from the matches. Take example 3; it sums up to 12 unit lengths of matches, which divides evenly into a 3 unit length square, but none of the matches can be used to create a 3 unit length side.
So the approach is:
- Sum the ints, divide by 4. If we get a non-integer result, fail early, because we can’t have fractional sticks.
- Then loop through the list, seeing if we can build sides of the target length using the given match lengths.
- If we can’t build four sides, fail. Otherwise, succeed.
Raku
The tricky part to this became exiting from the loop
sub canBuildSquare(@ints) {
my $side = @ints.sum / 4;
return False, '-' unless $side == $side.Int;
# sort the matches in descending length
@ints = @ints.sort.reverse;
# if the longest match is longer than the side length,
# we can't build a square
return False, '-' if @ints[0] > $side;
my @sides = ( [], ); # start with one empty side
while (True) {
my @unused;
for @ints -> $match {
# this match can be added to other matches to make a side
if ($match + @sides[*-1].sum <= $side) {
@sides[*-1].push($match); # add to the matches for this side
if (@sides[*-1].sum == $side && @sides.elems < 4) {
# if we've made a complete side, and we have < 4 sides
@sides.push([]); # add a new empty side
}
}
else {
@unused.push($match); # save match for later
}
}
# exit the loop if we didn't use any matches,
# or we've used ALL the matches
if (@unused.elems == @ints.elems || @unused.elems == 0) {
last;
}
# move the unused matches back to the list
@ints = @unused;
}
if (@sides.elems == 4) {
return True, @sides.map({ .join('+')}).join(', ');
}
return False, '-';
}
View the entire Raku script for this task on GitHub.
$ raku/ch-2.raku
Example 1:
Input: @ints = (1, 2, 2, 2, 1)
Output: True
Sides: 2, 2, 2, 1+1
Example 2:
Input: @ints = (2, 2, 2, 4)
Output: False
Sides: -
Example 3:
Input: @ints = (2, 2, 2, 2, 4)
Output: False
Sides: -
Example 4:
Input: @ints = (3, 4, 1, 4, 3, 1)
Output: True
Sides: 4, 4, 3+1, 1+3
Example 5:
Input: @ints = (3, 4, 2, 4, 3)
Output: False
Sides: -
Perl
The big thing to note is that we need List::AllUtils’ sum0
function to get the same functionality as Raku’s built-in .sum
.
use List::AllUtils qw( sum0 );
sub canBuildSquare(@ints) {
my $side = sum0(@ints) / 4;
return "False", '-' unless $side == int($side);
# sort the matches in descending length
@ints = reverse sort @ints;
# if the longest match is longer than the side length,
# we can't build a square
return "False", '-' if $ints[0] > $side;
my @sides = ( [], ); # start with one empty side
while (true) {
my @unused;
foreach my $match ( @ints ) {
# this match can be added to other matches to make a side
if ($match + sum0(@{ $sides[-1] }) <= $side) {
push @{ $sides[-1] }, $match; # add to the matches for this side
if (sum0(@{ $sides[-1] }) == $side && @sides < 4) {
# if we've made a complete side, and we have < 4 sides
push @sides, []; # add a new empty side
}
}
else {
push @unused, $match; # save match for later
}
}
# exit the loop if we didn't use any matches,
# or we've used ALL the matches
if (scalar(@unused) == scalar(@ints) || @unused == 0) {
last;
}
# move the unused matches back to the list
@ints = @unused;
}
if (@sides == 4) {
return "True", join(', ', map { join('+', @$_) } @sides);
}
return "False", '-';
}
View the entire Perl script for this task on GitHub.
Python
xxx
def canBuildSquare(ints):
side = sum(ints) / 4
if side != int(side):
return "False", '-'
# sort the matches in descending length
ints.sort(reverse = True)
# if the longest match is longer than the side length,
# we can't build a square
if ints[0] > side:
return "False", '-'
sides = [ [], ]; # start with one empty side
while True:
unused = []
for match in ints:
# this match can be added to other matches to make a side
if match + sum(sides[-1]) <= side:
sides[-1].append(match) # add to the matches for this side
if sum(sides[-1]) == side and len(sides) < 4:
# if we've made a complete side, and we have < 4 sides
sides.append([]) # add a new empty side
usedSomeMatch = True
else:
unused.append(match) # save match for later
# exit the loop if we didn't use any matches,
# or we've used ALL the matches
if len(ints) == len(unused) or len(unused) == 0:
break
# move the unused matches back to the list
ints = unused
if (len(sides) == 4):
return "True", ', '.join([
'+'.join([ str(m) for m in side] ) for side in sides
])
return "False", '-'
View the entire Python script for this task on GitHub.
Elixir
As always, Elixir’s functional nature made me think a bit harder about things. I keep getting tripped up on not being able to modify values from within a statement without returning that value from the statement, hence the littering of label = if (condition) do (some operation on label) else label end
throughout the code.
def loopThroughMatches([], %{
ints: ints, unused: unused, sides: sides
} = data) do
# return if we didn't use any matches,
# or we've used ALL the matches
if length(ints) == length(unused) or length(unused) == 0 do
if length(sides) == 4 do # did we build 4 sides?
{"True", sides}
else
{"False", '-'}
end
else
# move the unused matches back to the list
data = data
|> Map.put(:ints, unused)
|> Map.put(:unused, [])
# loop through the remaining matches
loopThroughMatches(unused, data)
end
end
def loopThroughMatches([match | remaining], %{
unused: unused, sideLen: sideLen, sides: sides
} = data) do
# this match can be added to other matches to make a side
data = if match + Enum.sum(List.last(sides)) <= sideLen do
# add to the matches for this side
this_side = List.last(sides) ++ [ match ]
# put this side back in the sides list
sides = List.replace_at(sides, -1, this_side)
# if we've made a complete side, and we have < 4 sides
sides =
if Enum.sum(this_side) == sideLen and length(sides) < 4 do
sides ++ [ [] ] # add a new empty side
else
sides
end
Map.put(data, :sides, sides)
else
unused = unused ++ [ match ] # save match for later
Map.put(data, :unused, unused)
end
loopThroughMatches(remaining, data)
end
def canBuildSquare(ints, sideLen) do
data = %{
ints: ints,
unused: [],
sideLen: sideLen,
sides: [ [] ] # start with one empty side
}
loopThroughMatches(ints, data)
end
def canBuildSquare(ints) do
# find the length of the sides of a square
sideLen = Enum.sum(ints) / 4
# sort the matches in descending length
ints = Enum.sort(ints, &(&1 >= &2))
{result, sides} = cond do
# sides aren't integer length
sideLen != Float.floor(sideLen) ->
{"False", '-'}
# if the longest match is longer than the side length,
# we can't build a square
List.first(ints) > sideLen ->
{"False", '-'}
# build sides from the matches
true -> canBuildSquare(ints, sideLen)
end
{
result,
if result == "True" do
Enum.map(sides, fn matches ->
Enum.join(matches, "+")
end)
|> Enum.join(", ")
else
sides
end
}
end
View the entire Elixir script for this task on GitHub.
Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-296/packy-anderson