Perl Weekly Challenge: Something just BROKE…

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

Leave a Reply