Perl Weekly Challenge: You’ve got to get up every morning…

Perl Weekly Challenge 375‘s tasks are “Single Common Word” and “Find K-Beauty”.

🤔 Ok, I’m free-associating here, but the “K” and “Beauty” made me think of Carol King and Beautiful.

Task 1: Single Common Word

You are given two array of strings.

Write a script to return the number of strings that appear exactly once in each of the two given arrays. String comparison is case sensitive.

Example 1

Input: @array1 = ("apple", "banana", "cherry")
       @array2 = ("banana", "cherry", "date")
Output: 2

Example 2

Input: @array1 = ("a", "ab", "abc")
       @array2 = ("a", "a", "ab", "abc")
Output: 2

"a" appears once in @array1 but appears twice in @array2, therefore, not counted.

Example 3

Input: @array1 = ("orange", "lemon")
       @array2 = ("grape", "melon")
Output: 0

Example 4

Input: @array1 = ("test", "test", "demo")
       @array2 = ("test", "demo", "demo")
Output: 0

Example 5

Input: @array1 = ("Hello", "world")
       @array2 = ("hello", "world")
Output: 1

String comparison is case sensitive.

Approach

We’re counting how many occurrences of an element there are in an array? Long-time readers of this blog will know what the first word that popped into my mind: Bag! A bag/multiset makes this easy: push the arrays into a bag, find keys that are in the intersection between the bags, and make sure the count in each bag is 1 for those keys.

Raku

I realized once I started coding, that it would be easier to create the bag, then elminate entries where the count was not 1, and then find the intersection. Of note is the .grep call after .Bag winds up converting my data structure from a Bag into a Set, but that’s fine, because all I needed the bag for was to count how many times each string occurs in the array. I could have done this as a one-liner

(@array1.Bag.grep({ $_.value == 1 }) ∩ @array2.Bag.grep({ $_.value == 1 })).elems


but I like the readability of the solution I went with.

sub singleCommonCount(@array1, @array2) {
  my %bag1 = @array1.Bag.grep({ $_.value == 1 });
  my %bag2 = @array2.Bag.grep({ $_.value == 1 });
  (%bag1%bag2).elems
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: @array1 = ("apple", "banana", "cherry")
       @array2 = ("banana", "cherry", "date")
Output: 2

Example 2:
Input: @array1 = ("a", "ab", "abc")
       @array2 = ("a", "a", "ab", "abc")
Output: 2

Example 3:
Input: @array1 = ("orange", "lemon")
       @array2 = ("grape", "melon")
Output: 0

Example 4:
Input: @array1 = ("test", "test", "demo")
       @array2 = ("test", "demo", "demo")
Output: 0

Example 5:
Input: @array1 = ("Hello", "world")
       @array2 = ("hello", "world")
Output: 1

Perl

I really wanted to use Set::Bag for the bag operations, but there really isn’t an easy way to make a Set::Bag object from a list without using List::MoreUtil’s frequency, and I was going to have to push the values through frequency once again once I filtered out the values that occurred more than once:

  my %bag1 = frequency @$array1;
  my $set1 = Set::Bag->new(frequency grep { $bag1{$_} == 1 } keys %bag1);
  my %bag2 = frequency @$array2;
  my $set2 = Set::Bag->new(frequency grep { $bag2{$_} == 1 } keys %bag2);
  scalar @{[ ($set1 & $set2)->elements ]};


So I just opted to use Set::Scalar instead. It’s no fewer lines, but it also affords me the size method to return how many elements, instead of just dumping a list of keys I have to count somehow. But then I looked at the repeated code and said, nah… I want to abstract that out into its own function. It cost me an extra line, but I think it looks cleaner.

use List::MoreUtils qw( frequency );
use Set::Scalar;

sub singleSet(@array) {
  my %bag = frequency @array;
  Set::Scalar->new( grep { $bag{$_} == 1 } keys %bag );
}

sub singleCommonCount($array1, $array2) {
  singleSet(@$array1)->intersection(singleSet(@$array2))->size;
}

View the entire Perl script for this task on GitHub.

Python

I decided to make the same sacrifice in my Python solution as well: add an extra line of code length to avoid duplicating the code that converts the list to a bag Counter, filter out items where the count isn’t 1, and then convert that to a set.

from collections import Counter

def single_set(array):
  return set([ k for k,v in Counter(array).items() if v == 1 ])

def single_common_count(array1, array2):
  return len(single_set(array1) & single_set(array2))

View the entire Python script for this task on GitHub.

Elixir

And I carried that over to the Elixir solution as well. In Elixir, Maps can function just like sets because of the Map.intersect/2 function, but because Enum.filter/2 produces a List instead of a Map, we need to pipe it through Map.new/1 to make it a Map again.

def single_set(array) do
  Enum.frequencies(array)
  |> Enum.filter(fn {_, v} -> v == 1 end)
  |> Map.new
end

def single_common_count(array1, array2) do
  Map.intersect(single_set(array1), single_set(array2))
  |> Map.keys |> length
end

View the entire Elixir script for this task on GitHub.


Task 2: Find K-Beauty

You are given a number and a digit (k).

Write a script to find the K-Beauty of the given number. The K-Beauty of an integer number is defined as the number of substrings of given number when it is read as a string has length of ‘k’ and is a divisor of given number.

Example 1

Input: $num = 240, $k = 2
Output: 2

Substring with length 2:
24: 240 is divisible by 24
40: 240 is divisible by 40

Example 2

Input: $num = 1020, $k = 2
Output: 3

Substring with length 2:
10: 1020 is divisible by 10
02: 1020 is divisible by 2
20: 1020 is divisible by 20

Example 3

Input: $num = 444, $k = 2
Output: 0

Substring with length 2:
First "44": 444 is not divisible by 44
Second "44": 444 is not divisible by 44

Example 4

Input: $num = 17, $k = 2
Output: 1

Substring with length 2:
17: 17 is divisible by 17

Example 5

Input: $num = 123, $k = 1
Output: 2

Substring with length 1:
1: 123 is divisible by 1
2: 123 is not divisible by 2
3: 123 is divisible by 3

Approach

Really, the extra text for the examples really gives away the approach: loop through the string making $k length substrings, and check to see if the number is evenly divisible by the number represented by the substring. We can check if x is evenly divisible by y if x mod y == 0.

Raku

I don’t have to cast the variables as .Str and .Int; one of the beauties of Perl/Raku family languages is that they’ll automatically coerce strings to integers and vice versa. But it’s so easy to do in Raku, and reads so cleanly, I feel like doing so makes it more Raku-ish.

sub k_beauty($num, $k) {
  my $count = 0;
  for 0..($num.Str.chars - $k) -> $i {
    $count++ if $num.Int mod $num.Str.substr($i, $k).Int == 0;
  }
  $count;
} 

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku
Example 1:
Input: $num = 240, $k = 2
Output: 2

Example 2:
Input: $num = 1020, $k = 2
Output: 3

Example 3:
Input: $num = 444, $k = 2
Output: 0

Example 4:
Input: $num = 17, $k = 2
Output: 1

Example 5:
Input: $num = 123, $k = 1
Output: 2

Perl

But in Perl, I’m fully taking advantage of the automatic coercion, because that feels more Perlish.

sub k_beauty($num, $k) {
  my $count = 0;
  for my $i ( 0..(length($num) - $k)) {
    $count++ if $num % substr($num, $i, $k) == 0;
  }
  $count;
}

View the entire Perl script for this task on GitHub.

Python

There isn’t any automatic coercion in Python, so I have to be explicit. And rather than coercing the number to a string multiple times, I’m coercing it once and storing it in numstr.

def k_beauty(num, k):
  count  = 0
  numstr = str(num)
  for i in range(len(numstr)-k+1):
    if num % int(numstr[i:i+k]) == 0:
      count += 1
  return count

View the entire Python script for this task on GitHub.

Elixir

There’s no automatic coercion in Elixir, either. I’m using Enum.reduce/3 to do the loop, and I’m pulling the substring into a label of it’s own mostly for readability, otherwise line 8 would be just too long.

def k_beauty(num, k) do
  numstr = num |> Integer.to_string
  Enum.reduce(0..(String.length(numstr) - k), 0, fn i, count ->
    sub = String.slice(numstr, i, k)
    if Integer.mod(num, String.to_integer(sub)) == 0 do
      count + 1
    else
      count
    end
  end)
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-375-packy-anderson/challenge-375/packy-anderson

Leave a Reply