There’s no relation to the problems for this week’s musical theme. Earlier this week, my wife and I watched the documentary Gordon Lightfoot: If You Could Read My Mind, so you’re getting my favorite Gordon Lightfoot song… Canadian Railroad Trilogy.
So now, while we’re listening to the construction of the Canadian Pacific Railway, let’s construct some code as part of Perl Weekly Challenge 280!
Task 1: Twice Appearance
You are given a string, $str
, containing lowercase English letters only.
Write a script to print the first letter that appears twice.
Example 1
Input: $str = "acbddbca"
Output: "d"
Example 2
Input: $str = "abccd"
Output: "c"
Example 3
Input: $str = "abcdabbb"
Output: "a"
Approach
At first, I thought I wanted to use a Bag for this but then I realized that Bags are good for counting items all at once. I don’t want a count of all the characters… I just want to find the first character that occurs more than once. So I dropped the fancy-pants object types, and just went with a hash.
I did, however, want to go above and beyond the examples. What if no character occurs more than once? I needed a fallback case.
Raku
sub twiceAppearance($str) {
my @chars = $str.comb;
my %count;
for @chars -> $char {
%count{$char}++;
return $char if %count{$char} > 1;
}
return "␀"; # fallback
}
View the entire Raku script for this task on GitHub.
$ raku/ch-1.raku
Example 1:
Input: $str = "acbddbca"
Output: "d"
Example 2:
Input: $str = "abccd"
Output: "c"
Example 3:
Input: $str = "abcdabbb"
Output: "a"
Example 4:
Input: $str = "abcdefg"
Output: "␀"
Perl
The Perl version of the script is just like the Raku version (or do I still write very Perl-ish Raku?).
sub twiceAppearance($str) {
my @chars = split //, $str;
my %count;
foreach my $char ( @chars ) {
$count{$char}++;
return $char if $count{$char} > 1;
}
return "␀"; # fallback
}
View the entire Perl script for this task on GitHub.
Python
In Python, I still want to use a Counter object, though, because they return a zero count for missing items instead of raising an error, which means we can just increment elements that don’t exist yet into existence (to use a Perl term, autovivification).
from collections import Counter
def twiceAppearance(str):
count = Counter()
for char in str:
count[char] += 1
if count[char] > 1:
return char
return "␀" # fallback
View the entire Python script for this task on GitHub.
Elixir
When I got to the Elixir solution, I realized I didn’t need to increment the value in the hash, er, Map. I just needed to test to see if the character was already in the map. If it was, I could return the character. If it wasn’t, I add the character to the map and process the next character.
def twiceAppearance([], _), do: "␀" # fallback
def twiceAppearance([next | rest], count) do
if Map.has_key?(count, next) do
next
else
twiceAppearance(rest, Map.put(count, next, 1))
end
end
def twiceAppearance(str) do
# split the string into characters and
# reprocess with an empty Map
twiceAppearance(String.graphemes(str), %{})
end
View the entire Elixir script for this task on GitHub.
Task 2: Count Asterisks
You are given a string, $str
, where every two consecutive vertical bars are grouped into a pair
.
Write a script to return the number of asterisks, *
, excluding any between each pair of vertical bars.
Example 1
Input: $str = "p|*e*rl|w**e|*ekly|"
Ouput: 2
The characters we are looking here are "p" and "w**e".
Example 2
Input: $str = "perl"
Ouput: 0
Example 3
Input: $str = "th|ewe|e**|k|l***ych|alleng|e"
Ouput: 5
The characters we are looking here are "th", "e**", "l***ych" and "e".
Approach
When I saw this one, it screamed regular expression to me. I could use a regex to remove characters between pairs of vertical bars, and then just count the asterisks.
Raku
Here’s where a Bag came in handy, because I could use it to get a count of all the different characters all at once. But because the $str
parameter is read-only (and I can’t make it read-write because I’m passing in a string constant), I need to copy it to another variable before I modify it. Fortunately the (my $newvar = $oldvar)
construction I learned in Perl still works in Raku.
sub countAsterisks($str) {
(my $excluded = $str) ~~ s:global/\| <-[ | ]> * \|//;
my %count = $excluded.comb.Bag;
return 0 unless %count{"*"}:exists;
return %count{"*"};
}
View the entire Raku script for this task on GitHub.
$ raku/ch-2.raku
Example 1:
Input: $str = "p|*e*rl|w**e|*ekly|"
Output: 2
Example 2:
Input: $str = "perl"
Output: 0
Example 3:
Input: $str = "th|ewe|e**|k|l***ych|alleng|e"
Output: 5
Edit: I’m reading through other solutions to these problems, and Laurent Rosenfield presented the following solution:
sub count-asterisks ($in is copy) {
$in ~~ s:g/'|'.*?'|'//;
return +($in ~~ tr/*//);
}
🤦🏻♂️ I don’t have to copy to a new variable! Also his regex is much cleaner. And he counts the asterisks in a similar fashion to what I wind up doing down in the Elixir solution.
Perl
Again, I’m writing my Raku with my Perl brain, so converting my Raku code back to Perl isn’t that hard. I do wish I had a Bag type in Perl… well, there’s a Set::Bag module in CPAN, but it looks more complex to use than what I need, so I’m just emulating a Bag with a hash.
sub countAsterisks($str) {
(my $excluded = $str) =~ s/\|[^|]*\|//g;
my %count;
map { $count{$_}++ } split //, $excluded;
return 0 unless exists $count{"*"};
return $count{"*"};
}
View the entire Perl script for this task on GitHub.
Python
Here’s where I got to use a Counter like a Bag: just pass the string to the counter after it’s been stripped by the regex. Here’s where the Counter returning a zero count for missing items really comes in handy.
from collections import Counter
import re
def countAsterisks(str):
count = Counter(re.sub(r'\|[^|]+\|', '', str))
return count["*"]
View the entire Python script for this task on GitHub.
Elixir
And just with task 1, I got all the way to the Elixir implementation before I realized I could do things a slightly different way: instead of counting the asterisks remaining in the string once I’d stripped out everything that was between pairs of vertical bars, I could just use another regex to strip everything that wasn’t an asterisk, and then just return the string length.
def countAsterisks(str) do
excluded = Regex.replace(~r/\|[^|]+\|/, str, "")
Regex.replace(~r/[^*]+/, excluded, "")
|> String.length
end
I only wish Regex.replace/4 took the string as the first parameter, because then the entire function could be a single pipe. In fact, I wanted to do this so much, I went searching, and I saw this statement:
It’s worth noting that you can use
String.replace(string, regex, replacement)
,String.split(string, regex)
, andString.match?(string, regex)
instead of the equivalent
Regex
functions.
🤦🏻♂️
def countAsterisks(str) do
str
|> String.replace(~r/\|[^|]+\|/, "")
|> String.replace(~r/[^*]+/, "")
|> String.length
end
MUCH BETTER.
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-280/packy-anderson