The show my wife is currently performing in is Stephen Sondheim’s Assassins (my favorite Sondheim musical), so with the first task being “Broken Keyboard”, I needed to use Something Just Broke…
But everybody’s got the right to be happy, and this week, what makes me happy is Perl Weekly Challenge 341.
Task 1: Broken Keyboard
You are given a string containing English letters only and also you are given a list of broken keys.
Write a script to return the total words in the given sentence can be typed completely.
Example 1
Input: $str = 'Hello World', @keys = ('d')
Output: 1
With broken key 'd', we can only type the word 'Hello'.
Example 2
Input: $str = 'apple banana cherry', @keys = ('a', 'e')
Output: 0
Example 3
Input: $str = 'Coding is fun', @keys = ()
Output: 3
No keys broken.
Example 4
Input: $str = 'The Weekly Challenge', @keys = ('a','b')
Output: 2
Example 5
Input: $str = 'Perl and Python', @keys = ('p')
Output: 1
Approach
This feels like a regular expression to me: we build a regular expression that’s a character class of the letters of the broken key list, split the string on whitespace to make a list of words, and then return a count of how many words do not match the regular expression. However, we need to handle a special case: if there are no letters in the @keys
array, then an alternation of no letters will match every string, so if @keys
is empty, we just return the count of words.
Unfortunately, this task also tickled my memory, and a quick search through my blog posts reveals this was also Task 1 for PWC 275! However, in that task, there wasn’t an example where there were NO broken keys, so the solution I did back then wouldn’t work for this task… so I had to tweak my solutions. I’m not wholesale changing them, though, because I hate inventing the wheel more than once.
Raku
In here, line 8 is the big change.
sub brokenKeys($str, @keys) {
my @words = $str.comb(/\S+/);
# if there are no broken keys,
# we can type all the words
return @words.elems if @keys == 0;
# build a character class
my $regex = '<[' ~ @keys.join ~ ']>';
# count how many words don't match the class
return ( ( @words.grep({! /:i <$regex>/ }) ).elems );
}
View the entire Raku script for this task on GitHub.
$ raku/ch-1.raku
Example 1:
Input: $str = 'Hello World', @keys = ('d')
Output: 1
Example 2:
Input: $str = 'apple banana cherry', @keys = ('a','e')
Output: 0
Example 3:
Input: $str = 'Coding is fun', @keys = ()
Output: 3
Example 4:
Input: $str = 'The Weekly Challenge', @keys = ('a','b')
Output: 2
Example 5:
Input: $str = 'Perl and Python', @keys = ('p')
Output: 1
Perl
sub brokenKeys($str, @keys) {
my @words = split /\s+/, $str;
# if there are no broken keys,
# we can type all the words
return scalar(@words) if @keys == 0;
# build a character class
my $regex = '[' . join('', @keys) . ']';
# count how many words don't match the class
return ( scalar( grep {! /$regex/i } @words) );
}
View the entire Perl script for this task on GitHub.
Python
import re
def broken_keys(mystr, keys):
words = mystr.split()
# if there are no broken keys,
# we can type all the words
if len(keys) == 0: return len(words)
# build a character class
regex = re.compile('[' + ''.join(keys) + ']', re.I)
# count how many words don't match the class
return( len([
word for word in words if not re.search(regex, word)
]) )
View the entire Python script for this task on GitHub.
Elixir
In the Elixir solution, I’m using the function defined with an empty key list to handle the conditional when there aren’t any broken keys.
def broken_keys(str, []) do
# if there are no broken keys,
# we can type all the words
String.split(str)
|> Enum.count
end
def broken_keys(str, keys) do
# build a character class
{:ok, regex} = Regex.compile(
"[" <> Enum.join(keys) <> "]", [:caseless]
)
# count how many words don't match the class
String.split(str)
|> Enum.filter(fn word -> not Regex.match?(regex, word) end)
|> Enum.count
end
View the entire Elixir script for this task on GitHub.
Task 2: Reverse Prefix
You are given a string, $str
and a character in the given string, $char
.
Write a script to reverse the prefix upto the first occurrence of the given $char in the given string $str and return the new string.
Example 1
Input: $str = "programming", $char = "g"
Output: "gorpramming"
Reverse of prefix "prog" is "gorp".
Example 2
Input: $str = "hello", $char = "h"
Output: "hello"
Example 3
Input: $str = "abcdefghij", $char = "h"
Output: "hgfedcbaij"
Example 4
Input: $str = "reverse", $char = "s"
Output: "srevere"
Example 5
Input: $str = "perl", $char = "r"
Output: "repl"
Approach
This task felt new, however. In most languages, there’s a function to return the first occurrence of a character in a string, so we use that, then extract the substring from the start to that character, reverse it, and put it back in the original string.
Raku
In Raku, that function is the .index
method on the Str class. When I tested it with strings like "programming".index("g")
, I got back 3 as a result, so I knew that worked. I knew I could use Str’s .flip
method to reverse the string, so I just needed to pull the appropriate substrings out with Str’s substr
routine. But when I tried "programming".substr(0, 3)
, I got back the result pro
. Reading the documentation a little more carefully, I found that the third argument to substr
was the number of characters to return, not the ending position. Fortunately, that meant that I could just add 1 to what I got back from index
and it would be the value I needed to use in both calls.
I could have made this into a single line, but I didn’t want to call $str.index($char)
twice to get the same value.
sub reversePrefix($str, $char) {
my $loc = $str.index($char) + 1;
$str.substr(0, $loc).flip ~ $str.substr($loc);
}
View the entire Raku script for this task on GitHub.
$ raku/ch-2.raku
Example 1:
Input: $str = "programming", $char = "g"
Output: "gorpramming"
Example 2:
Input: $str = "hello", $char = "h"
Output: "hello"
Example 3:
Input: $str = "abcdefghij", $char = "h"
Output: "hgfedcbaij"
Example 4:
Input: $str = "reverse", $char = "s"
Output: "srevere"
Example 5:
Input: $str = "perl", $char = "r"
Output: "repl"
Perl
I don’t know why I was surprised by Raku’s behavior, though. The third argument to Perl’s substr
has always been the length. When we pair it with index
and reverse
, we get basically the same solution.
sub reversePrefix($str, $char) {
my $loc = index($str, $char) + 1;
reverse(substr($str, 0, $loc)) . substr($str, $loc);
}
View the entire Perl script for this task on GitHub.
Python
In Python, it’s not called index, it’s called find
. But the thing that still messes with my head after programming in Perl for 30+ years is that the way to extract substrings from is slicing notation. So to extract the first four characters from “programming”, the syntax is "programming"[0:4]
. The other thing that’s trippy is that the way to reverse a string is to step through the string backwards with slice notation, using [::-1]
. So "programming"[0:4][::-1]
yields “gorp”.
def reverse_prefix(mystr, char):
loc = mystr.find(char) + 1
return mystr[0:loc][::-1] + mystr[loc:]
View the entire Python script for this task on GitHub.
Elixir
Elixir, however, is the trippiest. Elixir does not have an index
function to find the location of a substring within a string. The general consensus is that one should use String.split/3
with parts: 2
to get the portions of a string around the substring you’re searching for. This winds up dropping the character itself, but that’s easy enough to add back.
Oh, and String.reverse/1
does the string reversal.
def reverse_prefix(str, char) do
[a, b] = String.split(str, char, parts: 2)
char <> String.reverse(a) <> b
end
View the entire Elixir script for this task on GitHub.
I mean, this methodology could have been used in the other languages as well!
sub reversePrefix($str, $char) {
my ($a, $b) = $str.split($char, 2);
$char ~ $str.flip ~ $b;
}
sub reversePrefix($str, $char) {
my ($a, $b) = split($char, $str, 2);
$char . reverse($a) . $b;
}
def reverse_prefix(mystr, char):
a, char, b = mystr.partition(char)
return char + a[::-1] + b
Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-341/packy-anderson