Even though Perl Weekly Challenge 352‘s tasks “Match String” and “Binary Prefix” ought to have been fertile ground for a musical theme related to the tasks, my brain was just too preoccupied with a certain someone’s 100th birthday celebration, I couldn’t think of any other music besides The Doodlin’ Song.
Task 1: Match String
You are given an array of strings.
Write a script to return all strings that are a substring of another word in the given array in the order they occur.
Example 1
Input: @words = ("cat", "cats", "dog", "dogcat", "dogcat", "rat", "ratcatdogcat")
Output: ("cat", "dog", "dogcat", "rat")
Example 2
Input: @words = ("hello", "hell", "world", "wor", "ellow", "elloworld")
Output: ("hell", "world", "wor", "ellow")
Example 3
Input: @words = ("a", "aa", "aaa", "aaaa")
Output: ("a", "aa", "aaa")
Example 4
Input: @words = ("flower", "flow", "flight", "fl", "fli", "ig", "ght")
Output: ("flow", "fl", "fli", "ig", "ght")
Example 5
Input: @words = ("car", "carpet", "carpenter", "pet", "enter", "pen", "pent")
Output: ("car", "pet", "enter", "pen", "pent")
Approach
I’m sure there’s a clever way to do this, but I’m going with the straight ahead double loop: loop over each word ($i), and have a nested loop within that looping over the words again ($j) that skips comparing when $i = $j so we don’t match strings against themselves. We’ll check to see if @words[$i] is a substring of @words[$j] using some kind of index function (a regular expression is too heavy a lift for just seeing whether one string is a substring of another), and, if it is, we push @words[$i] unto a result array and then skip the rest of the $j loop, since all we need is that it’s a substring of ONE of the other strings.
Raku
When I implemented it in Raku, I made a discovery: the first example yielded the results ("cat", "dog", "dogcat". I realized that was because we matched , "dogcat", "rat")"dogcat" as a result when $i = 3, $j = 4, we matched it again when $i = 4, $j = 3. Looking at the other examples, it was clear that the problem was supposed to match earlier elements, otherwise "hell" would never match as a substring of "hello". So I needed to keep track of words as I compared in the $i loop, and skip them if they came up again in the $i loop. That’s when I introduced lines 6, 9, & 10.
sub matchString(@words) {
my @matches;
my %seen;
for 0..@words.end -> $i {
my $substring = @words[$i];
# skip if we've already considered this substring
next if %seen{$substring}++;
for 0..@words.end -> $j {
# we're not matching substrings against themselves
next if $i == $j;
my $string = @words[$j];
# skip $string if $substring doesn't match
next if $string.index($substring) === Nil;
# save the matched substring
@matches.push($substring);
# it matches at least one, we don't have to check the rest
last;
}
}
return @matches;
}View the entire Raku script for this task on GitHub.
$ raku/ch-1.raku
Example 1:
Input: @words = ("cat", "cats", "dog", "dogcat", "dogcat", "rat",
"ratcatdogcat")
Output: ("cat", "dog", "dogcat", "rat")
Example 2:
Input: @words = ("hello", "hell", "world", "wor", "ellow", "elloworld")
Output: ("hell", "world", "wor", "ellow")
Example 3:
Input: @words = ("a", "aa", "aaa", "aaaa")
Output: ("a", "aa", "aaa")
Example 4:
Input: @words = ("flower", "flow", "flight", "fl", "fli", "ig", "ght")
Output: ("flow", "fl", "fli", "ig", "ght")
Example 5:
Input: @words = ("car", "carpet", "carpenter", "pet", "enter", "pen", "pent")
Output: ("car", "pet", "enter", "pen", "pent")Perl
As usual, going from Raku to Perl was just fixing the for to foreach syntax, making the postfix method calls into standalone function calls, and converting the @arr[] to $arr[] and %hash{} to $hash{}.
sub matchString(@words) {
my @matches;
my %seen;
foreach my $i ( 0 .. $#words ) {
my $substring = $words[$i];
# skip if we've already considered this substring
next if $seen{$substring}++;
foreach my $j ( 0 .. $#words ) {
# we're not matching substrings against themselves
next if $i == $j;
my $string = $words[$j];
# skip $string if $substring doesn't match
next if index($string, $substring) < 0;
# save the matched substring
push @matches, $substring;
# it matches at least one, we don't have to check the rest
last;
}
}
return @matches;
}View the entire Perl script for this task on GitHub.
Python
Going from Raku to Python was similarly easy: ditching all the sigils, remembering that declaring an empty array is [] not () (that’s a tuple), and making the postfix conditionals into normal conditionals. One thing that was good to know: while str.find() will return the kind of info Raku’s Str.index() does the documentation has a caveat:
The
find()method should be used only if you need to know the position of sub. To check if sub is a substring or not, use theinoperator:'Py' in 'Python' True
def match_string(words):
matches = []
seen = {}
for i in range(len(words)):
substring = words[i]
# skip if we've already considered this substring
if seen.get(substring): continue
seen[substring] = 1 # can't test then update like in Perl
for j in range(len(words)):
# we're not matching substrings against themselves
if i == j: continue
string = words[j]
# skip $string if $substring doesn't match
if not substring in string: continue
# save the matched substring
matches.append(substring)
# it matches at least one, we don't have to check the rest
break
return matchesView the entire Python script for this task on GitHub.
Elixir
And because we’re potentially bailing out of a loop early, in Elixir I had to implement the loops as recursive functions. I chose to use different names for the outer and inner loop functions: match_string/5 for the i loop, and match_substring/6 for the j loop.
The bailing out of the j loop happens in lines 18-20.
# we've looped over all the words using j
def match_substring(_, _, j, len, matches, seen)
when j == len, do: {matches, seen}
# we're not matching substrings against themselves
def match_substring(words, i, j, len, matches, seen)
when i == j do
match_substring(words, i, j+1, len, matches, seen)
end
def match_substring(words, i, j, len, matches, seen) do
substring = Enum.at(words, i)
string = Enum.at(words, j)
if String.contains?(string, substring) do
matches = matches ++ [substring]
# it matches at least one, we don't have to check the rest
{matches, seen}
else
match_substring(words, i, j+1, len, matches, seen)
end
end
# we've looped over all the words using i
def match_string(_, i, len, matches, _)
when i==len, do: matches
def match_string(words, i, len, matches, seen) do
substring = Enum.at(words, i)
{matches, seen} = if Map.get(seen, substring) do
# skip if we've already considered this substring
match_substring(
words, i+1, 0, len, matches, seen
)
else
# add substring to seen for future iterations
match_substring(
words, i, 0, len, matches, Map.put(seen, substring, 1)
)
end
match_string(words, i+1, len, matches, seen)
end
def match_string(words) do
match_string(words, 0, length(words), [], %{})
endView the entire Elixir script for this task on GitHub.
Task 2: Binary Prefix
You are given an array, @nums, where each element is either 0 or 1.
Define xi as the number formed by taking the first i+1 bits of @nums (from $nums[0] to $nums[i]) and interpreting them as a binary number, with $nums[0] being the most significant bit.
For example:
If @nums = (1, 0, 1), then:
x0 = 1 (binary 1)
x1 = 2 (binary 10)
x2 = 5 (binary 101)
For each i, check whether xi is divisible by 5.
Write a script to return an array @answer where $answer[i] is true if x<sub>i</sub> is divisible by 5, otherwise false.
Example 1
Input: @nums = (0,1,1,0,0,1,0,1,1,1)
Output: (true, false, false, false, false, true, true, false, false, false)
Binary numbers formed (decimal values):
0: 0
01: 1
011: 3
0110: 6
01100: 12
011001: 25
0110010: 50
01100101: 101
011001011: 203
0110010111: 407
Example 2
Input: @num = (1,0,1,0,1,0)
Output: (false, false, true, true, false, false)
1: 1
10: 2
101: 5
1010: 10
10101: 21
101010: 42
Example 3
Input: @num = (0,0,1,0,1)
Output: (true, true, false, false, true)
0: 0
00: 0
001: 1
0010: 2
00101: 5
Example 4
Input: @num = (1,1,1,1,1)
Output: (false, false, false, true, false)
1: 1
11: 3
111: 7
1111: 15
11111: 31
Example 5
Input: @num = (1,0,1,1,0,1,0,0,1,1)
Output: (false, false, true, false, false, true, true, true, false, false)
1: 1
10: 2
101: 5
1011: 11
10110: 22
101101: 45
1011010: 90
10110100: 180
101101001: 361
1011010011: 723
Approach
Usually, the second task is harder than the first one, but I had an insight that made me realize it could be way easier: when we add another bit from @nums, the new number is just twice the old number plus the value of the bit. So I start off with an empty string ($bits) to represent the binary number, a numeric variable initialized to 0 ($num) to represent the decimal value, and then loop through @nums and do ($num * 2) + $bit and then append $bit to $bits, calculate whether $num is divisible by 5, and then push that onto a result array.
Raku
To be honest, the Raku documentation didn’t make it easy to find the * formatting for sprintf; I had to resort to reading Perl’s sprintf documentation to find it (I knew there was a way to specify the width dynamically, I just couldn’t remember what character it was). Though, once I knew what I was looking for, I finally found it.
sub binaryPrefix(@nums) {
my $bitwidth = @nums.elems;
my $bits = q{};
my $num = 0;
my $explain = q{};
my @output;
for @nums -> $bit {
$num = ($num * 2) + $bit;
$bits ~= $bit;
$explain ~= sprintf("\n%*s: %d", $bitwidth, $bits, $num);
@output.push($num % 5 == 0 ?? 'true' !! 'false');
}
return $explain, @output.join(", ");
}View the entire Raku script for this task on GitHub.
$ raku/ch-2.raku
Example 1:
Input: @nums = (0, 1, 1, 0, 0, 1, 0, 1, 1, 1)
Output: (true, false, false, false, false, true, true, false, false,
false)
Binary numbers formed (decimal values):
0: 0
01: 1
011: 3
0110: 6
01100: 12
011001: 25
0110010: 50
01100101: 101
011001011: 203
0110010111: 407
Example 2:
Input: @nums = (1, 0, 1, 0, 1, 0)
Output: (false, false, true, true, false, false)
Binary numbers formed (decimal values):
1: 1
10: 2
101: 5
1010: 10
10101: 21
101010: 42
Example 3:
Input: @nums = (0, 0, 1, 0, 1)
Output: (true, true, false, false, true)
Binary numbers formed (decimal values):
0: 0
00: 0
001: 1
0010: 2
00101: 5
Example 4:
Input: @nums = (1, 1, 1, 1, 1)
Output: (false, false, false, true, false)
Binary numbers formed (decimal values):
1: 1
11: 3
111: 7
1111: 15
11111: 31
Example 5:
Input: @nums = (1, 0, 1, 1, 0, 1, 0, 0, 1, 1)
Output: (false, false, true, false, false, true, true, true, false,
false)
Binary numbers formed (decimal values):
1: 1
10: 2
101: 5
1011: 11
10110: 22
101101: 45
1011010: 90
10110100: 180
101101001: 361
1011010011: 723Perl
Even though I wrote this in Raku first, it was really already in Perl.
sub binaryPrefix(@nums) {
my $bitwidth = @nums;
my $bits = q{};
my $num = 0;
my $explain = q{};
my @output;
foreach my $bit ( @nums ) {
$num = ($num * 2) + $bit;
$bits .= $bit;
$explain .= sprintf("\n%*s: %d", $bitwidth, $bits, $num);
push @output, ($num % 5 == 0 ? 'true' : 'false');
}
return $explain, join(", ", @output);
}View the entire Perl script for this task on GitHub.
Python
Getting the dynamic width for the “Binary numbers formed” output was the big challenge here. As far as I can tell, there isn’t a way to pass it in as a parameter. So instead, I built a format string before my loop where the width of the bits is “hard coded” into the format string.
def binary_prefix(nums):
fmt = '\n{:>'+str(len(nums))+'}: {}'
bits = ''
num = 0
explain = ''
output = []
for bit in nums:
num = (num * 2) + bit
bits += str(bit)
explain += fmt.format(bits, num)
output.append('true' if num % 5 == 0 else 'false')
return explain, ", ".join(output)View the entire Python script for this task on GitHub.
Elixir
Elixir, on the other hand, made right-justifying the bits easy: String.pad_leading/3. There’s only one recursive function, and I don’t need to bail from it early.
def binary_prefix([], _, _,_, explain, output),
do: {explain, Enum.join(output, ", ")}
def binary_prefix([bit | nums], bitwidth, bits, num,
explain, output)
do
num = (num * 2) + bit
bits = bits <> to_string(bit)
padded = String.pad_leading(bits, bitwidth)
explain = explain <> "\n#{padded}: #{num}"
value = if rem(num,5) == 0, do: "true", else: "false"
output = output ++ [ value ]
binary_prefix(nums, bitwidth, bits, num, explain, output)
end
def binary_prefix(nums) do
binary_prefix(nums, length(nums), "", 0, "", [])
endView 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-352/packy-anderson