Perl Weekly Challenge: Just like honey, baby, from the bee

Perl Weekly Challenge 380‘s tasks are “Sum of Frequencies” and “Reverse Degree”.

For music…

She’s as sweet as Tupelo honey
She’s an angel of the first degree…

Task 1: Sum of Frequencies

You are given a string consisting of English letters.

Write a script to find the vowel and consonant with maximum frequency. Return the sum of two frequencies.

Example 1

Input: $str = "banana"
Output: 5

Vowel: "a" appears 3 times.
Consonant: "n" appears 2 times, "b" appears 1 time.

Max frequency of vowel: 3
Max frequency of consonant: 2

Example 2

Input: $str = "teestett"
Output: 7

Vowel: "e" appears 3 times.
Consonant: "t" appears 4 times, "s" appears 1 time.

Max frequency of vowel: 3
Max frequency of consonant: 4

Example 3

Input: $str = "aeiouuaa"
Output: 3

Vowel: "a" appears 3 times, "u" 2 times, "e", "i", "o" 1 time each.
Consonant: None.

Max frequency of vowel: 3
Max frequency of consonant: 0

Example 4

Input: $str = "rhythm"
Output: 2

Vowel: None
Consonant: "h" appears 2 times, "r", "y", "t", "m" 1 time each.

Max frequency of vowel: 0
Max frequency of consonant: 2

Example 5

Input: $str = "x"
Output: 1

Vowel: None
Consonant: "x" appears 1 time.

Max frequency of vowel: 0
Max frequency of consonant: 1

Approach

We’re counting frequencies, so of course we’re going to use my favorite data structure, a bag. We split the string into characters, filter vowels into one bag, filter consonants into a different bag, then get the character with the maximum value from each.

Raku

In Raku, we’re using .comb to pull out the vowels/consonants, the .Bag coercer to turn the list into a bag, .values to get the counts, and .max to get the maximum value from that count list. The problem we run into is if the maximum value list is empty because there were no vowels or consonants in a particular string, .max produces -Inf. Fortunately, a pass through the procedural form of max with 0 handles that case.

sub sumFrequencies($str) {
  my $max_vowel     = $str.comb(/ <[aeiou]>/).Bag.values.max;
  my $max_consonant = $str.comb(/<-[aeiou]>/).Bag.values.max;
  # if a list is empty, the max is -Inf
  return max($max_vowel, 0) + max($max_consonant, 0);
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: $str = "banana"
Output: 5

Example 2:
Input: $str = "teestett"
Output: 7

Example 3:
Input: $str = "aeiouuaa"
Output: 3

Example 4:
Input: $str = "rhythm"
Output: 2

Example 5:
Input: $str = "x"
Output: 1

Perl

For the Perl solution, I remembered what Oriel Jutty said about my PWC 378 Perl solution, and I used $str =~ /pattern/g to get the individual characters matching the pattern. Sadly, I couldn’t feed the list produced by List::MoreUtil’s frequency, directly into values, I had to save them in a hash first. But I was able to add the minimum value 0 to lines 10 & 11, so I didn’t need to have two more max-es on line 12.

use List::AllUtils  qw( max );
use List::MoreUtils qw( frequency );

sub sumFrequencies($str) {
  my %vowels        = frequency $str =~ / [aeiou]/gx;
  my %consonants    = frequency $str =~ /[^aeiou]/gx;
  my $max_vowel     = max(values(%vowels), 0);
  my $max_consonant = max(values(%consonants), 0);
  return $max_vowel + $max_consonant;
}

View the entire Perl script for this task on GitHub.

Python

I had a similar problem in Python. If I tried to directly grab .most_common(1) from the Counter and it didn’t have any elements, I got an empty list back instead of a list with a tuple of (char, count). So, to handle the case where the Counter is empty, I add an element with the key 0 that has the value 0. This way, if the Counter was empty, .most_common(1) yields (0, 0).

from collections import Counter

def sum_frequencies(string):
  vowels     = Counter([ c for c in string if c     in "aeiou" ])
  consonants = Counter([ c for c in string if c not in "aeiou" ])
  vowels[0]     = 0 # if there weren't any counted,
  consonants[0] = 0 # set the count to 0
  return (
    vowels.most_common(1)[0][1] + consonants.most_common(1)[0][1]
  )

View the entire Python script for this task on GitHub.

Elixir

In Elixir, I’m following the same algorithm, but had to be more verbose in places. I particularly liked my idea (somewhat stolen from the Python implementation) of using String.contains?/2 to determine if a letter was a vowel (this being the equivalent of in "aeiou"). Enum.frequencies/1 and Map.values/1 do what you’d expect them to, but Enum.max/3 has a feature where you can pass a function as the third parameter to use if the list is empty, which is what we’ve been working around in all the other solutions! The second parameter, &>=/2, is the default second parameter, and it basically says that we’re numerically comparing the values in the list and keeping each value in the list that’s greater than or equal to the previous value in the list.

def sum_frequencies(str) do
  max_vowels = String.graphemes(str)
  |> Enum.filter(fn c -> String.contains?("aeiou", c) end)
  |> Enum.frequencies
  |> Map.values
  |> Enum.max(&>=/2, fn -> 0 end)
  max_consonants = String.graphemes(str)
  |> Enum.filter(fn c -> not String.contains?("aeiou", c) end)
  |> Enum.frequencies
  |> Map.values
  |> Enum.max(&>=/2, fn -> 0 end)
  max_vowels + max_consonants
end

View the entire Elixir script for this task on GitHub.


Task 2: Reverse Degree

You are given a string.

Write a script to find the reverse degree of the given string.

For each character, multiply its position in the reversed alphabet (‘a’ = 26, ‘b’ = 25, …, ‘z’ = 1) with its position in the string. Sum these products for all characters in the string to get the reverse degree.

Example 1

Input: $str = "z"
Output: 1

Reverse alphabet value of "z" is 1.
Position 1: 1 x 1
Sum of product: 1

Example 2

Input: $str = "a"
Output: 26

Reverse alphabet value of "a" is 26.
Position 1: 1 x 26
Sum of product: 26

Example 3

Input: $str = "bbc"
Output: 147

Reverse alphabet value of "b" is 25 and "c" is 24.
Position 1: 1 x 25
Position 2: 2 x 25
Position 3: 3 x 24
Sum of product: 25 + 50 + 72 => 147

Example 4

Input: $str = "racecar"
Output: 560

Reverse alphabet value of "r" is 9, "a" is 26, "c" is 24 and "e" is 24.
Position 1: 1 x 9
Position 2: 2 x 26
Position 3: 3 x 24
Position 4: 4 x 22
Position 5: 5 x 24
Position 6: 6 x 26
Position 7: 7 x 9
Sum of product: 9 + 52 + 72 + 88 + 120 + 156 + 63

Example 5

Input: $str = "zyx"
Output: 14

Reverse alphabet value of "z" is 1, "y" is 2 and "x" is 3.
Position 1: 1 x 1
Position 2: 2 x 2
Position 3: 3 x 3
Sum of product: 1 + 4 + 9

Approach

I could take the ordinal value of each character and do some math to get its reversed position (123 - ord($char)), but instead I’m taking my inspiration for this task from the Python and Elixir solutions to task 1; in that task, I checked to see if a character was a vowel by checking to see if it existed in a string of vowels. Now I’m going to get the position of the letter in a reversed alphabet by looking up the index of the character in the string of the reversed alphabet.

Raku

I could have just used the statement on line 5 instead of the function call on line 12, but I wanted the code to be a little more readable. We’re using Str.index to get the position of the character in the reversed alphabet, and the usual Str.comb to split the string.

sub reversedPos($char) {
  "zyxwvutsrqponmlkjihgfedcba".index($char) + 1;
}

sub reverseDegree($str) {
  my @chars = $str.comb;
  my $sum = 0;
  for 0..@chars.end -> $i {
    $sum += (reversedPos(@chars[$i]) * ($i+1));
  }
  $sum;
}

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku
Example 1:
Input: $str = "z"
Output: 1

Example 2:
Input: $str = "a"
Output: 26

Example 3:
Input: $str = "bbc"
Output: 147

Example 4:
Input: $str = "racecar"
Output: 560

Example 5:
Input: $str = "zyx"
Output: 14

Perl

The Perl solution is pretty much the same; it uses index and split.

sub reversedPos($char) {
  index("zyxwvutsrqponmlkjihgfedcba", $char) + 1;
}

sub reverseDegree($str) {
  my @chars = split //, $str;
  my $sum = 0;
  for my $i ( 0 .. $#chars ) {
    $sum += (reversedPos($chars[$i]) * ($i+1));
  }
  $sum;
}

View the entire Perl script for this task on GitHub.

Python

Normally, in Python I’d use str.find to return the position of a substring within a string because if the substring isn’t found, it returns the value -1. However, even though str.index raises a ValueError when the substring is not found, I’m using it here because there isn’t a chance that the character won’t be found, and I like the rhyming with the Perl and Raku solutions.

def reversed_pos(char):
  return "zyxwvutsrqponmlkjihgfedcba".index(char) + 1

def reverse_degree(string):
  pos = 1
  sum = 0
  for char in string:
    sum += (reversed_pos(char) * pos)
    pos += 1
  return sum

View the entire Python script for this task on GitHub.

Elixir

Sadly, there isn’t a function in Elixir to find the position of a character in a string. Fortunately, it’s not that hard to emulate by using String.graphemes/1 to split the reversed alphabet into a list of graphemes (something that would be perceived as a single character by readers), then use Enum.find_index/2 to get the index of the character in that list, and then use Kernel.then/2 to add 1 to the resulting value.

This gives us our reversed_pos/1 function. Now we use String.graphemes/1 again to split the string into graphemes, Enum.reduce/3 with a tuple accumulator of {sum, pos} to add up the reversed positions multiplied by the positions, and then use Kernel.elem/2 to grab only the sum portion of the accumulator.

def reversed_pos(char), do:
  String.graphemes("zyxwvutsrqponmlkjihgfedcba")
  |> Enum.find_index(fn c -> c == char end)
  |> then( &( &1+1 ) )

def reverse_degree(str) do
  String.graphemes(str)
  |> Enum.reduce({0, 1}, fn c, {sum, pos} ->
    { sum + (reversed_pos(c) * pos), pos+1 }
  end)
  |> elem(0)
end

View the entire Elixir script for this task on GitHub.


Here’s all my solutions in GitHub: https://github.com/packy/perlweeklychallenge-club/tree/challenge-380-packy-anderson/challenge-380/packy-anderson

Leave a Reply