Unequally Common
with Raku

by Arne Sommer

Unequally Common with Raku

[254] Published 17. September 2023.

This is my response to The Weekly Challenge #234.

Challenge #234.1: Common Characters

You are given an array of words made up of alphabetic characters only.

Write a script to return all alphabetic characters that show up in all words including duplicates.

Example 1:
Input: @words = ("java", "javascript", "julia")
Output: ("j", "a")
Example 2:
Input: @words = ("bella", "label", "roller")
Output: ("e", "l", "l")
Example 3:
Input: @words = ("cool", "lock", "cook")
Output: ("c", "o")

Coercing each word into a Set (with .Set), thus removing duplicates, and taking the intersection of those sets (with the Intersection Operator or the ascii version (&)) gives the common characters:

> say "java".comb.Set (&) "javascript".comb.Set (&) "julia".comb.Set
Set(a j)

See docs.raku.org/type/Set for more information about the Set type.

See docs.raku.org/routine/(&), infix ∩ for more information about the intersection operator.

We can actually skip the .Set calls, as the intersection operater does the right thing anyway.

> say "java".comb (&) "javascript".comb (&) "julia".comb
Set(a j)
File: common-characters-set
#! /usr/bin/env raku

unit sub MAIN (*@words where @words.elems > 0       # [1]
                 && all(@words) ~~ /^<[a..z]>+$/,   # [1a]
               :v(:$verbose));

my @sets   = @words>>.comb>>.Set;                   # [2]
my @common = ( [(&)] @sets ).keys;                  # [3]

say ":Sets: { @sets.raku }" if $verbose;

say "({ @common.map( '"' ~ * ~ '"').join(", ") })"; # [4]

[1] Ensure at least one element, and they must consist of english lower case letters only; and at least one letter.

[2] Get the sets. We can skip this call, as shown above, but the verbose output uses the sets.

[3] Get the intersection. The result is s set, so we apply keys to get the characters.

[4] Print the common characters.

Running it:

$ ./common-characters-set java javascript julia
("j", "a")

$ ./common-characters-set bella label roller
("l", "e")

$ ./common-characters-set cool lock cook
("c", "o")

The second one is wrong, but let us do verbose mode before explaining why:

$ ./common-characters-set -v java javascript julia
:Sets: [Set.new("v","j","a"),
        Set.new("j","i","t","c","v","r","p","s","a"),
        Set.new("i","a","u","j","l")]
("j", "a")

$ ./common-characters-set -v bella label roller
:Sets: [Set.new("l","e","a","b"),
        Set.new("e","a","l","b"),
        Set.new("l","e","r","o")]
("l", "e")

$ ./common-characters-set -v cool lock cook
:Sets: [Set.new("o","c","l"),
        Set.new("l","o","k","c"),
        Set.new("o","c","k")]
("o", "c")

The problem is the duplicates, as the Set does not handle them.

But we can use the Bag type instead, as it has a concept of weight.

File: common-characters-bag
#! /usr/bin/env raku

unit sub MAIN (*@words where @words.elems > 0
                 && all(@words) ~~ /^<[a..z]>+$/,
               :v(:$verbose));

my @bags   = @words>>.comb>>.Bag;   # [1]
my @common = ( [(&)] @bags ).kxxv;  # [2]

say ":Bags: { @bags.raku }" if $verbose;

say "({ @common.map( '"' ~ * ~ '"').join(", ") })";

[1] Turn the words into the bag type this time.

[2] We have to keep the weight, and the kxxv operator does just that (gives a list with the keys added as many times as their weight implies).

See docs.raku.org/routine/kxxv for more information about kxxv.

Note that I mentioned kxxv in Words of a Sort, the weekly challenge from last week. Then I could not actually use it, but this week it pans out. Fancy that...

$ ./common-characters-bag -v java javascript julia
:Bags: [("v"=>1,"j"=>1,"a"=>2).Bag,
        ("t"=>1,"a"=>2,"r"=>1,"v"=>1,"j"=>1,"p"=>1,"s"=>1,"c"=>1,"i"=>1).Bag,
        ("i"=>1,"j"=>1,"l"=>1,"a"=>1,"u"=>1).Bag]
("a", "j")

$ ./common-characters-bag -v bella label roller
:Bags: [("e"=>1,"b"=>1,"a"=>1,"l"=>2).Bag,
        ("b"=>1,"a"=>1,"l"=>2,"e"=>1).Bag,
        ("l"=>2,"r"=>2,"e"=>1,"o"=>1).Bag]
("e", "l", "l")

$ ./common-characters-bag -v cool lock cook
:Bags: [("o"=>2,"l"=>1,"c"=>1).Bag,
        ("o"=>1,"l"=>1,"k"=>1,"c"=>1).Bag,
        ("k"=>1,"c"=>1,"o"=>2).Bag]
("o", "c")

Looking good...

Except the order of the characters in the last one. Hash like structures does not have a concept of order, so running the same command a second time will give a different random order:

$ ./common-characters-bag -v cool lock cook
:Bags: [("c"=>1,"o"=>2,"l"=>1).Bag,
        ("o"=>1,"c"=>1,"l"=>1,"k"=>1).Bag,
        ("o"=>2,"c"=>1,"k"=>1).Bag]
("c", "o")

Looking a the examples shows that the output order is given by the first word.

File: common-characters
#! /usr/bin/env raku

unit sub MAIN (*@words where @words.elems > 0
                 && all(@words) ~~ /^<[a..z]>+$/,
               :v(:$verbose));

my @bags   = @words>>.comb>>.Bag;
my @common = ( [(&)] @bags ).kxxv;

say ":Bags: { @bags.raku }" if $verbose;

my $common = @common.BagHash;                        # [1]
my @sorted;                                          # [2]               

for @words[0].comb -> $letter                        # [3]
{
  say ": Considering first word letter $letter" if $verbose;

  next unless $common{$letter};                      # [4]
  $common{$letter}--;                                # [5]
  @sorted.push: $letter;                             # [6]

  say ": - Added letter $letter (remaining count: $common{$letter})"
   if $verbose;
}

say "({ @sorted.map( '"' ~ * ~ '"').join(", ") })";  # [7]

[1] Start with the common letters and give us a BagHash, a mutable (writeable) version of a Bag - as we are going to change it later on (in [5]).

[2] The common letters will end up here, in the correct sorted order.

[3] Iterate over the letters in the first word, giving the correct sorting order.

[4] Skip the letter if it does not appear in the BagHash.

[5] We have used one instance of the letter; there may be more.

[6] Add the letter to the result.

[7] Print the correctly sorted result.

Running it:

$ ./common-characters -v java javascript julia
:Bags: [("j"=>1,"v"=>1,"a"=>2).Bag, ("j"=>1,"p"=>1,"c"=>1,"i"=>1,"a"=>2,"t"=>1,"v"=>1,"r"=>1,"s"=>1).Bag, ("l"=>1,"a"=>1,"u"=>1,"j"=>1,"i"=>1).Bag]
: Considering first word letter j
: - Added letter j (remaining count: 0)
: Considering first word letter a
: - Added letter a (remaining count: 0)
: Considering first word letter v
: Considering first word letter a
("j", "a")

$ ./common-characters -v bella label roller
:Bags: [("l"=>2,"b"=>1,"e"=>1,"a"=>1).Bag, ("b"=>1,"a"=>1,"l"=>2,"e"=>1).Bag, ("l"=>2,"e"=>1,"r"=>2,"o"=>1).Bag]
: Considering first word letter b
: Considering first word letter e
: - Added letter e (remaining count: 0)
: Considering first word letter l
: - Added letter l (remaining count: 1)
: Considering first word letter l
: - Added letter l (remaining count: 0)
: Considering first word letter a
("e", "l", "l")

$ ./common-characters -v cool lock cook
:Bags: [("l"=>1,"c"=>1,"o"=>2).Bag, ("c"=>1,"o"=>1,"l"=>1,"k"=>1).Bag, ("o"=>2,"c"=>1,"k"=>1).Bag]
: Considering first word letter c
: - Added letter c (remaining count: 0)
: Considering first word letter o
: - Added letter o (remaining count: 0)
: Considering first word letter o
: Considering first word letter l
("c", "o")

Challenge #234.2: Unequal Triplets

You are given an array of positive integers.

Write a script to find the number of triplets (i, j, k) that satisfies num[i] != num[j], num[j] != num[k] and num[k] != num[i].

Example 1:
Input: @ints = (4, 4, 2, 4, 3)
Ouput: 3

(0, 2, 4) because 4 != 2 != 3
(1, 2, 4) because 4 != 2 != 3
(2, 3, 4) because 2 != 4 != 3
Example 2:
Input: @ints = (1, 1, 1, 1, 1)
Ouput: 0
Example 3:
Input: @ints = (4, 7, 1, 10, 7, 4, 1, 1)
Output: 28

triplets of 1, 4, 7  = 3x2×2 = 12 combinations
triplets of 1, 4, 10 = 3×2×1 = 6  combinations
triplets of 4, 7, 10 = 2×2×1 = 4  combinations
triplets of 1, 7, 10 = 3x2x1 = 6 combinations
File: unequal-triplets
#! /usr/bin/env raku

unit sub MAIN (*@ints where all(@ints) ~~ Int && all(@ints) > 0,  # [1]
               :v(:$verbose));

my $bag          = @ints.Bag;                                     # [2]
my $combinations = 0;                                             # [3]

if $bag.keys >= 3                                                 # [4]
{
  my @unique = @ints.unique.sort;                                 # [5]
  for @unique.combinations(3) -> @combination                     # [6]
  {
    my $add = [*] @combination.map({ $bag{$_} });                 # [7]

    say ": Triplet: ({ @combination.join(",") }) has $add combinations"
      if $verbose;

    $combinations += $add;                                        # [8]
  }
}

say $combinations;                                                # [9]

[1] An array consisting of positive integers only. Note that an empty array is allowed.

[2] Turn the input into a Bag.

[3] The total number of combinations will end up here.

[4] Only compute the total if we have three or more unique values. If not, the answer will be zero.

[5] Get the unique values, in sorted order (so that we will get the same sorted result each time we run the program). Note that we could have used the Bag instead; $bag.keys.sort.

[6] Iterate over all the possible 3-element combinations of the unique values (with combinations).

[7] Each positive integer in the combination is replaced with the weight (with map), and we multiply them all together with the Reduction Metaoperator [].

[8] Add that sum from [7] to the total.

[9] Print the result (the total number of unique combinations).

Running it:

$ ./unequal-triplets 4 4 2 4 3
3

$ ./unequal-triplets 1 1 1 1 1
0

$ ./unequal-triplets 4 7 1 10 7 4 1 1
28

Looking good.

With verbose mode:

$ ./unequal-triplets -v 4 4 2 4 3
: Triplet: (2,3,4) has 3 combinations
3

$ ./unequal-triplets -v 1 1 1 1 1
0

$ ./unequal-triplets -v 4 7 1 10 7 4 1 1
: Triplet: (1,4,7) has 12 combinations
: Triplet: (1,4,10) has 6 combinations
: Triplet: (1,7,10) has 6 combinations
: Triplet: (4,7,10) has 4 combinations
28

And that's it.