Perl Weekly Challenge: Time to count B chars

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 a Seq of non-overlapping matches limited to at most $limit matches.

If $matcher is a Regex, each Match object is converted to a Str, 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, both round(0.5) and round(-0.5) are 0, and round(1.5) is 2).

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

Leave a Reply