This Perl Weekly Challenge is all about algorithmic replacement, and in thinking about the second task I realized that algorithmic replacement was also possible in music.
So let’s modulate, hold a high note, and see if we can hear the title of Perl Weekly Challenge 330.
Task 1: Clear Digits
You are given a string containing only lower case English letters and digits.
Write a script to remove all digits by removing the first digit and the closest non-digit character to its left.
Example 1
Input: $str = "cab12"
Output: "c"
Round 1: remove "1" then "b" => "ca2"
Round 2: remove "2" then "a" => "c"
Example 2
Input: $str = "xy99"
Output: ""
Round 1: remove "9" then "y" => "x9"
Round 2: remove "9" then "x" => ""
Example 3
Input: $str = "pa1erl"
Output: "perl"
Approach
This solution, like so many recently, screams for a regular expression. We can easily match a “digit and the closest non-digit character to its left” with the regex \D\d
(\d
is a single decimal digit, and \D
is the negation of that character class). By repeatedly applying the substitution until it doesn’t match anymore, we can remove each occurrence (note, we can’t do it with just one pass through the regex because the characters being removed aren’t always adjacent during the first pass through).
Raku
So the Raku solution is really short:
sub clearDigits($str is copy) {
while ($str ~~ s/\D\d//) {}
return $str;
}
View the entire Raku script for this task on GitHub.
$ raku/ch-1.raku
Example 1:
Input: $str = "cab12"
Output: "c"
Example 2:
Input: $str = "xy99"
Output: ""
Example 3:
Input: $str = "pa1erl"
Output: "perl"
If, however, I wanted to produce the exact format that Mohammad had in his task definition, I could actually expand the regex to capture the values and use the block in the while
loop to print them out:
sub clearDigits($str is copy) {
my $count = 0;
while ($str ~~ s/(\D)(\d)//) {
$count++;
say qq{Round $count: remove "$1" then "$0" => "$str"};
}
return $str;
}
$ raku/ch-1.raku
Example 1:
Input: $str = "cab12"
Round 1: remove "1" then "b" => "ca2"
Round 2: remove "2" then "a" => "c"
Output: "c"
Example 2:
Input: $str = "xy99"
Round 1: remove "9" then "y" => "x9"
Round 2: remove "9" then "x" => ""
Output: ""
Example 3:
Input: $str = "pa1erl"
Round 1: remove "1" then "a" => "perl"
Output: "perl"
Perl
With Perl, I needed to make two changes: I didn’t need to make the $str
parameter a copy, since it already is in Perl, and I needed to change Raku’s smartmatch operator ~~
to Perl’s binding operator =~
.
sub clearDigits($str) {
while ($str =~ s/\D\d//) {}
return $str;
}
View the entire Perl script for this task on GitHub.
Elixir
While I was reading the Raku documentation on regex substitution to see if there was a way to have the substitution performed repeatedly (perhaps through an adverb) I ran across the following phrase in the explanation for the global adverb: “substitutions are non-recursive“, and I immediately knew how I was going to do the Elixir solution.
However, since Regex.replace/4
returns the value of the modified string (unlike Raku and Perl, which modify the string in place and returns a boolean value indicating whether or not the substitution succeeded) (though Raku does have the S///
operator for non-destructive substitution), I did need to use Regex.match?/2
to check if we need to do more substitution before making the recursive call.
def clear_digits(str) do
if Regex.match?(~r/\D\d/, str),
do: clear_digits(Regex.replace(~r/\D\d/,str,"")),
else: str
end
View the entire Elixir script for this task on GitHub.
Python
This time, jumping to the Elixir solution immediately after the Perl solution influenced the way I did the Python solution: namely, with recursion. I did compile the regex first, though, because it gave me the nice syntactic sugar of being able to call the .search
and .sub
methods on the compiled regex.
import re
cd = re.compile(r'\D\d')
def clear_digits(strVal):
if cd.search(strVal):
strVal = clear_digits(cd.sub("", strVal))
return strVal
I also noticed while looking at Python’s More Control Flow Tools there’s a section at the end called 4.10. Intermezzo: Coding Style that says
Name your classes and functions consistently; the convention is to use
UpperCamelCase
for classes andlowercase_with_underscores
for functions and methods.
I guess I should follow the convention, like I am for Elixir.
View the entire Python script for this task on GitHub.
Task 2: Title Capital
You are given a string made up of one or more words separated by a single space.
Write a script to capitalise the given title. If the word length is 1 or 2 then convert the word to lowercase otherwise make the first character uppercase and remaining lowercase.
Example 1
Input: $str = "PERL IS gREAT"
Output: "Perl is Great"
Example 2
Input: $str = "THE weekly challenge"
Output: "The Weekly Challenge"
Example 3
Input: $str = "YoU ARE A stAR"
Output: "You Are a Star"
Approach
Easy-peasy. Lowercase the entire string, split the string into words on whitespace, check the length of the word and if its greater than 2, swap the case of the first letter to uppercase. Rejoin the resulting words with a single space. Done.
Raku
When I first saw this problem, I thought “Raku has a titlecase function!”. But then I looked at Str
‘s routine tc
, and I realized it was just like Perl’s ucfirst
: it coverts the first character of the string, not of each word.
sub titleCapital($str) {
my @out = $str.lc.split(" "); # separated by a SINGLE space
for @out <-> $word { # <-> makes mods to $word change @out
$word = $word.tc if $word.chars > 2;
}
return @out.join(" ");
}
View the entire Raku script for this task on GitHub.
$ raku/ch-2.raku
Example 1:
Input: $str = "PERL IS gREAT"
Output: "Perl is Great"
Example 2:
Input: $str = "THE weekly challenge"
Output: "The Weekly Challenge"
Example 3:
Input: $str = "YoU ARE A stAR"
Output: "You Are a Star"
Perl
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.
I wonder whether Mohammad had this in mind when he wrote that the words are separated by a single space.
Anyway, the Perl is exactly like the Raku, except none of the functions are postfix modifiers and the foreach
loop doesn’t need any special syntax to make changes to $word
modify the array it’s looping over.
sub titleCapital($str) {
my @out = split " ", lc($str); # separated by a SINGLE space
foreach my $word ( @out ) { # mods to $word change @out
$word = ucfirst($word) if length($word) > 2;
}
return join(" ", @out);
}
View the entire Perl script for this task on GitHub.
Elixir
Elixir makes this task slightly easier, though, because String.capitalize/2
“converts the first character in the given string to uppercase and the remainder to lowercase”.
def title_capital(str) do
str
|> String.split # separated by a SINGLE space
|> Enum.map(fn w ->
if String.length(w) > 2,
do: String.capitalize(w),
else: String.downcase(w)
end)
|> Enum.join(" ")
end
View the entire Elixir script for this task on GitHub.
Python
In Python, though, I was able to get it down to essentially a one-liner (though I broke it up into multiple lines for legibility). Things to note:
- If
Str.split
is called without a separator, 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. - I’m using a list comprehension to loop over the words and build the list passed to the final
Str.join
.
def titleCapital(strVal):
return " ".join(
[
w.title() if len(w) > 2 else w
for w in strVal.lower().split()
]
)
View the entire Python script for this task on GitHub.
Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-330/packy-anderson