Perl Weekly Challenge: You have the last word, Buddy…

This Perl Weekly Challenge has tasks “Last Word” and “Buddy Strings”, and that got me thinking about the spate of famous people dying lately, so I decided to give the “last word” to someone known not for words but for… his flugelhorn.

So now let’s see why Perl Weekly Challenge 331 feels so good…

Task 1: Last Word

You are given a string.

Write a script to find the length of last word in the given string.

Example 1

Input: $str = "The Weekly Challenge"
Output: 9

Example 2

Input: $str = "   Hello   World    "
Output: 5

Example 3

Input: $str = "Let's begin the fun"
Output: 3

Approach

As soon as I saw Mohammad‘s example 2 for this task, I flashed back to last week… cheesey “doo-doo-doo-doo-doo-doo” flashback sound and wavy video effect

LAST WEEK:
The perldoc split documentation has an interesting little bit:

As another special case, split /PATTERN/,EXPR,LIMIT emulates the default behavior of the command line tool awk when the PATTERN is either omitted or a string composed of a single space character (such as ' ' or "\x20", but not e.g. / /). In this case, any leading whitespace in EXPR is removed before splitting occurs, and the PATTERN is instead treated as if it were /\s+/; in particular, this means that any contiguous whitespace (not just a single space character) is used as a separator.

However, this special treatment can be avoided by specifying the pattern / / instead of the string " ", thereby allowing only a single space character to be a separator.

BACK IN THE PRESENT:
Heh. So when you run split q{ }, " Hello World "; in Perl, you get back an array containing Hello and World, with none of the extra whitespace. This task was designed for this function.

Perl

So this week I’m doing Perl first. It’s basically a one liner, so I’m going to pick it apart:

split q{ }, $str

This splits the string on whitespace, removing any leading and trailing whitespace, returning a list of words.

+(...)[-1]

takes the list represented by ... and returns the last element, and

length(...)

returns the length of the string passed in as an argument.

sub lastWord($str) {
  return length(+(split q{ }, $str)[-1]);
}

View the entire Perl script for this task on GitHub.

$ perl/ch-1.pl
Example 1:
Input: $str = "The Weekly Challenge"
Output: 9

Example 2:
Input: $str = "   Hello   World    "
Output: 5

Example 3:
Input: $str = "Let's begin the fun"
Output: 3

Raku

The Raku solution looks pretty much exactly the same, except the functions are postfix method calls: Str.split with the :skip-empty parameter, the *-1 syntax for getting the last element of a list, and .chars to determine the number of graphemes in the string.

sub lastWord($str) {
  return $str.split(q{ }, :skip-empty)[*-1].chars;
}

View the entire Raku script for this task on GitHub.

One of the things I found while looking for the documentation on *-1… an explanation for WHY IT WORKS LIKE IT DOES!!!

Note: The asterisk, which is actually a Whatever, is important. Passing a bare negative integer (e.g. @alphabet[-1]) like you would do in many other programming languages, throws an error in Raku.

What actually happens here, is that an expression like *-1 declares a code object via Whatever-priming – and the [ ] subscript reacts to being given a code object as an index, by calling it with the length of the collection as argument and using the result value as the actual index. In other words, @alphabet[*-1] becomes @alphabet[@alphabet.elems - 1].

This means that you can use arbitrary expressions which depend on the size of the collection:Raku highlighting

say @array[* div 2];  # select the middlemost element
say @array[$i % *];   # wrap around a given index ("modular arithmetic")
say @array[ -> $size { $i % $size } ];  # same as previous

This is the kind of stuff I really think is cool. Your milage may vary.

Python

Just like Perl, Python’s str.split() has special behavior:

If sep is not specified or is None, a different splitting algorithm is applied: runs of consecutive whitespace are regarded as a single separator, and the result will contain no empty strings at the start or end if the string has leading or trailing whitespace. Consequently, splitting an empty string or a string consisting of just whitespace with a None separator returns [].

For example:Copy

'1 2 3'.split()
['1', '2', '3']
'1 2 3'.split(maxsplit=1)
['1', '2 3']
'   1   2   3   '.split()
['1', '2', '3']
def last_word(strVal):
  return len(strVal.split()[-1])

View the entire Python script for this task on GitHub.

Elixir

String.split/1 divides a string into substrings at each Unicode whitespace occurrence with leading and trailing whitespace ignored (and groups of whitespace treated as a single occurrence), List.last/2 returns the last element of that list, and String.length/1 returns the number of graphemes in the string.

  def last_word(str) do
    str |> String.split |> List.last |> String.length
  end

View the entire Elixir script for this task on GitHub.


Task 2: Buddy Strings

You are given two strings, source and target.

Write a script to find out if the given strings are Buddy Strings.

If swapping of a letter in one string make them same as the other then they are `Buddy Strings`.

Example 1

Input: $source = "fuck"
       $target = "fcuk"
Output: true

The swapping of 'u' with 'c' makes it buddy strings.

Example 2

Input: $source = "love"
       $target = "love"
Output: false

Example 3

Input: $source = "fodo"
       $target = "food"
Output: true

Example 4

Input: $source = "feed"
       $target = "feed"
Output: true

Approach

This task is making me think a little harder. There is, of course, the brute force way to do it: take the source string, loop through it and swap adjacent characters and test to see if matches the target string. In the worst case where the source cannot produce the target through swapping, we’d wind up doing … for a string n characters long, we’d wind up doing n-1 swaps. Now that I think about it, that’s not that bad; it’s still an O(n) operation.

Raku

Really, once I realized I really was just going to be swapping adjacent characters, this fell together pretty easily. Split the source string into an array so it’s easy to swap characters without actually moving them around by using a list slice, use the tried and true ($a, $b) = ($b, $a) method of swapping two values in Perl-ish languages, and then join the list together on an empty string to compare with the target.

sub buddyString($source, $target) {
  # put the source characters in an array
  my @source = $source.comb;
  # loop over the first to all but last characters
  for 0 .. @source.elems - 2 -> $i {
    # generate a list of character positions
    my @slice = (0 .. @source.elems - 1);
    # swap the $i-th and following positions
    (@slice[$i], @slice[$i+1]) = (@slice[$i+1], @slice[$i]);
    # test to see if it matches the target!
    return True if @source[@slice].join('') eq $target;
  }
  # womp-womp! nothing matched!
  return False;
}

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku
Example 1:
Input: fuck = "fuck"
       fcuk = "fcuk"
Output: True

Example 2:
Input: love = "love"
       love = "love"
Output: False

Example 3:
Input: fodo = "fodo"
       food = "food"
Output: True

Example 4:
Input: feed = "feed"
       feed = "feed"
Output: True

Perl

The Perl solution is pretty much the same as the Raku solution, except we use split instead of .comb, we have to return stringy true and false, and the sigils for accessing array elements is $ instead of @.

sub buddyString($source, $target) {
  # put the source characters in an array
  my @source = split //, $source;
  # loop over the first to all but last characters
  foreach my $i (0 .. $#source - 1) {
    # generate a list of character positions
    my @slice = (0 .. $#source);
    # swap the $i-th and following positions
    ($slice[$i], $slice[$i+1]) = ($slice[$i+1], $slice[$i]);
    # test to see if it matches the target!
    return 'true' if join('', @source[@slice]) eq $target;
  }
  # womp-womp! nothing matched!
  return 'false';
}

View the entire Perl script for this task on GitHub.

Python

Python wasn’t much different, I just needed to remember that range(n) goes from 0 to n-1, and array slicing in Python is geared towards slices in order (either forward or reverse), but I was able to quickly pivot to a list comprehension to build the array of characters in an arbitrary order to pass to join.

def buddy_string(source, target):
  # put the source characters in an array
  src = list(source)
  # loop over the first to all but last characters
  for i in range(len(src) - 1):
    # generate a list of character positions
    slice = list(range(len(src)))
    # swap the $i-th and following positions
    (slice[i], slice[i+1]) = (slice[i+1], slice[i])
    # test to see if it matches the target!
    if ''.join([ src[c] for c in slice ]) == target:
      return True
  # womp-womp! nothing matched!
  return False

View the entire Python script for this task on GitHub.

Elixir

Imaging my amazement when I discovered that there was an Elixir function, Enum.slide/3, that “lides a single or multiple elements” from an enumerable from one spot to another!

Usually, when I want to do a loop in Elixir, I want to use recursion. In this case, recursion is a good candidate because it lets me bail out of the search before going through all the possible iterations, while the other tool I use to implement loops in Elixir, Enum.map_reduce/3, makes you loop over the entire enumerable. As with all the other solutions, I break the source string into a list of characters before I go into the loop, and that’s good, because I’m able to make the terminal condition of the recursion a guard on the function definition from lines 5-6 (Kernel.length/1 is allowed in guards, but String.length/1 is not).

The bailing early from the recursion occurs on lines 12-13, where we determine that swapping these two particular characters in source cause it to match target, and the recursive call to check the next character is on line 16.

  # womp-womp! nothing matched!
  def buddy_string(source, _, i) when i >= length(source)-1,
    do: "false"

  def buddy_string(source, target, i) do
    # swap the i-th and following positions
    # and re-join the list into a string, then
    # test to see if it matches the target!
    if target == Enum.slide(source, i, i+1) |> Enum.join do
      "true"
    else
      # look starting with the next character
      buddy_string(source, target, i+1)
    end
  end

  def buddy_string(source, target) do
    # put the source characters in a list, and
    # process from the beginning of the list
    buddy_string(String.graphemes(source), target, 0)
  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-331/packy-anderson