Musical free association: “B after A” became “time to play B sides…“
This week’s challenge is all about characters: counting occurrences of a character in a string and returning what percentage of the string it is, and determining if one of two characters occurs in a string after the last occurrence of the other character.
Without further ado, Perl Weekly Challenge 273!
Task 1: Percentage of Character
You are given a string, $str
and a character $char
.
Write a script to return the percentage, nearest whole, of given character in the given string.
Example 1
Input: $str = "perl", $char = "e"
Output: 25
Example 2
Input: $str = "java", $char = "a"
Output: 50
Example 3
Input: $str = "python", $char = "m"
Output: 0
Example 4
Input: $str = "ada", $char = "a"
Output: 67
Example 5
Input: $str = "ballerina", $char = "l"
Output: 22
Example 6
Input: $str = "analitik", $char = "k"
Output: 13
Approach
We need to find two pieces of information: the string’s length, and how many times the specified character occurs. String length is built in, and we can easily count the character occurs by splitting the string into an array and filtering the array for just the character we want.
Oh, and analitik? I had to look it up. And it turns out Ballerina is a programming language, too!
Raku
I’m using .comb
like split, then .grep
to match the character, and .elems
to count how many matches we got.
sub charPercent($str, $char) {
my $char_cnt = $str.comb.grep({ $_ eq $char }).elems;
return round(( $char_cnt / $str.chars ) * 100);
}
$ raku/ch-1.raku
Example 1:
Input: $str = "perl", $char = "e"
Output: 25
Example 2:
Input: $str = "java", $char = "a"
Output: 50
Example 3:
Input: $str = "python", $char = "m"
Output: 0
Example 4:
Input: $str = "ada", $char = "a"
Output: 67
Example 5:
Input: $str = "ballerina", $char = "l"
Output: 22
Example 6:
Input: $str = "analitik", $char = "k"
Output: 13
But wait… the .comb
routine on the Str
type doesn’t just work like split:
Searches for
$matcher
in$input
and returns aSeq
of non-overlapping matches limited to at most$limit
matches.If
$matcher
is a Regex, eachMatch
object is converted to aStr
, unless$match
is set .If no matcher is supplied, a Seq of characters in the string is returned, as if the matcher was
rx/./
.
Really, the way I’ve been using .comb
for all these months has been the “no matcher” case, but what if I used $matcher
? I could get the function down to essentially one line:
sub charPercent($str, $char) {
return round(( $str.comb($char).elems / $str.chars ) * 100);
}
View the entire Raku script for this task on GitHub.
Perl
Perl’s round
comes from the POSIX module.
use POSIX qw(round);
sub charPercent($str, $char) {
my $char_cnt = scalar( grep { $_ eq $char } split //, $str );
return round( ($char_cnt / length($str)) * 100 );
}
View the entire Perl script for this task on GitHub.
Python
In Python, I ran into a quirk of round
:
For the built-in types supporting
round()
, values are rounded to the closest multiple of 10 to the power minus ndigits; if two multiples are equally close, rounding is done toward the even choice (so, for example, bothround(0.5)
andround(-0.5)
are0
, andround(1.5)
is2
).
This means that round(12.5)
isn’t 13, as I would expect it to be:
$ python
Python 3.10.4 (main, Jun 2 2022, 17:11:44) [Clang 13.0.0 (clang-1300.0.27.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> round(12.5)
12
def charPercent(strVal, charVal):
char_cnt = len([ c for c in strVal if c == charVal ])
return int( ( ( char_cnt / len(strVal) ) * 100 ) + 0.5 )
View the entire Python script for this task on GitHub.
Elixir
def charPercent(strVal, charVal) do
char_cnt = strVal
|> String.graphemes
|> Enum.filter(fn c -> c == charVal end)
|> length
trunc(round( ( char_cnt / String.length(strVal) ) * 100 ))
end
View the entire Elixir script for this task on GitHub.
Task 2: B After A
You are given a string, $str
.
Write a script to return true
if there is at least one b
, and no a
appears after the first b
.
Example 1
Input: $str = "aabb"
Output: true
Example 2
Input: $str = "abab"
Output: false
Example 3
Input: $str = "aaa"
Output: false
Example 4
Input: $str = "bbb"
Output: true
Approach
If we loop over the characters and keep track of if we’ve seen a b
, then if we see an a
after that, the script should return False
. Otherwise, when we reach the end of the characters, we return whether we saw a b
or not.
Raku
There’s probably a more concise way to do this, but I’m going for readability.
sub bAfterA($str) {
my $seen_b = False;
for $str.comb -> $c {
if ($seen_b) {
if ($c eq 'a') {
return False;
}
}
elsif ($c eq 'b') {
$seen_b = True;
}
}
return $seen_b;
}
$ raku/ch-2.raku
Example 1:
Input: $str = "aabb"
Output: True
Example 2:
Input: $str = "abab"
Output: False
Example 3:
Input: $str = "aaa"
Output: False
Example 4:
Input: $str = "bbb"
Output: True
View the entire Raku script for this task on GitHub.
Perl
As of Perl 5.40, the builtin
module is no longer experimental, and this means we now have built-in boolean true
and false
values in Perl, so we no longer have to use values like 0
to represent false and 1
to represent true.
use builtin ':5.40';
sub bAfterA($str) {
my $seen_b = false;
foreach my $c (split //, $str) {
if ($seen_b) {
if ($c eq 'a') {
return false;
}
}
elsif ($c eq 'b') {
$seen_b = true;
}
}
return $seen_b;
}
$ perl/ch-2.pl
Example 1:
Input: $str = "aabb"
Output: 1
Example 2:
Input: $str = "abab"
Output:
Example 3:
Input: $str = "aaa"
Output:
Example 4:
Input: $str = "bbb"
Output: 1
Ah, but it’s still stored internally as 1
and the empty string, so to get nice output, I need to do
say 'Output: ' . (bAfterA($str) ? 'True' : 'False');
$ perl/ch-2.pl
Example 1:
Input: $str = "aabb"
Output: True
Example 2:
Input: $str = "abab"
Output: False
Example 3:
Input: $str = "aaa"
Output: False
Example 4:
Input: $str = "bbb"
Output: True
View the entire Perl script for this task on GitHub.
Python
def bAfterA(strVal):
seen_b = False
for c in strVal:
if seen_b:
if c == 'a':
return False
elif c == 'b':
seen_b = True
return seen_b
View the entire Python script for this task on GitHub.
Elixir
Once again, I need to remind myself that to loop over a list in Elixir, I want recursion.
def bAfterA([], seen_b), do: seen_b
def bAfterA(, seen_b) do
cond do
seen_b && c == "a" -> false
true -> bAfterA(rest, seen_b || c == "b")
end
end
def bAfterA(strVal) when is_binary(strVal) do
bAfterA(String.codepoints(strVal), false)
end
The last bAfterA
definition has a Guard that says when the function is called with one argument and is_binary
(it’s a string), this is the definition we use. We then call the function recursively with two arguments: a list of characters, and a value for whether we’ve seen a b
or not.
The first bAfterA
definition handles our stopping case: the list has been exhausted and is now empty, so we should just return the value of seen_b
that we’ve been passing along.
The middle definition does all the heavy lifting: if we’ve seen a b
and the character we’re currently examining is an a
, then we immediately return false
. Otherwise, we process the remaining characters in the list, flipping the value of seen_b
if the current character happens to be a b
.
I could have done the middle definition like this
def bAfterA(, seen_b) do
if seen_b && c == "a" do
false
else
bAfterA(rest, seen_b || c == "b")
end
end
but I really like using cond
this way because it emphasizes that so much in Elixir are expressions, and not really flow control statements.
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-273/packy-anderson