Perl Weekly Challenge: Week 273

Challenge 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

Raku has the really handy .classify() method for sorting a list into a hash. First we need the list though so we use .comb() to split $str into individual characters.

$str.comb.classify({ $_ }, into => my %freq);

Now thanks to .classify() we have a hash, %freq whose keys are distinct characters from $str and whose values are the occurrences of that character. If $chr is a key of %freq (which we determine with .exists()) we can find the frequency by counting the values of that key with .elems(). This is divided by the length of $str (which we determine with .chars()) and multiplied by 100 to give the percentage occurrence of $chr in $str. The spec wants the percentage rounded to the nearest whole number which is easily accomplished with .round().

If $chr did not exist in %freq, 0 is printed.

say %freq{$chr}:exists ?? (%freq{$chr}.elems / $str.chars * 100).round !! 0;

(Full code on Github.)

This is the Perl version.

As we do not have .classify() we have to emulate it in these two lines. instead of adding occurrences to the hash, we can just keep a running count of the occurrences.

my %freq;
map { $freq{$_}++; } split //, $str;

We don't have a .round() method. Rather than write my own I just used the one in the Math::Round module. so we have to include use Math::Round qw/ round /; at the top of the script.

say exists $freq{$chr} ? round($freq{$chr} / (length $str) * 100) : 0;

(Full code on Github.)

Challenge 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

This one seemed very easy to do with a regular expression; /b[^a]*/ in Perl syntax. First we match the literal character b followed by 0 or more instances of any character except a.

However there is a catch. Take a look at example 3; it should output false because the first b is followed by an a but the way the regular expression was written above, the second b will match. My solution was to use 'back references'. These allow us to refer to earlier matched parts of a regular expression from later parts. b is wrapped in parentheses so it becomes a group. \1 is a reference to that group so that \1[^a]* is now guaranteed to match the first b followed by any characters except a only.

This is a one-liner that implements the whole solution. I was surprised that shift doesn't return $_ by default as many Perl functions do and I had to explicitly assign it. Perl has many annoying quirks like thia.

$_ = shift; say /(b)\1[^a]*/ ? "true" : "false"

(Full code on Github.)

This is the Raku version. Back references start with 0 instead of 1. Negated character chlasses use - instead of ^. .match() does not return true/false so we have to use .so() to convert it into a Boolean value.

@*ARGS[0].match(/(b)$0<[-a]>*/).so.say

(Full code on Github.)