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 viaWhatever
-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 aNone
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