Merged Maximum
with Raku

by Arne Sommer

Merged Maximum with Raku

[276] Published 12. February 2024.

This is my response to The Weekly Challenge #256.

Fun Fact about 256

256 is the same as:
2 ** 8
2*2*2*2*2*2*2*2
[*](2,2,2,2,2,2,2,2)
[*](<2 2 2 2 2 2 2 2>)

Challenge #256.1: Maximum Pairs

You are given an array of distinct words, @words.

Write a script to find the maximum pairs in the given array. The words $words[i] and $words[j] can be a pair one is reverse of the other.

Example 1:
Input: @words = ("ab", "de", "ed", "bc")
Output: 1

There is one pair in the given array: "de" and "ed"
Example 2:
Input: @words = ("aa", "ba", "cd", "ed")
Output: 0
Example 3:
Input: @words = ("uv", "qp", "st", "vu", "mn", "pq"))
Output: 2
File: maximum-pairs
#! /usr/bin/env raku

unit sub MAIN (*@words where @words.elems > 0              # [1]
                  && @words.elems == @words.unique.elems,  # [1a]
               :v(:$verbose));

my $words = @words.Set;                                    # [2]
my $count = 0;                                             # [3]

for $words.keys.sort -> $word                              # [4]
{
  my $reverse = $word.flip;                                # [5]

  if $words{$reverse}                                      # [6]
  {
    $count++;                                              # [6a]
    say ": + Word $word has a reverse ({$reverse})" if $verbose;
  }
  elsif ($verbose)
  {
    say ":   Word $word does not have a reverse" if $verbose;
  }
}

say $count div 2;                                          # [7]

[1] At least one word, and ensure that they are all unique [1a].

[2] Turn the list of words into a Set, a read only hash like structure. This makes it easy to look up values (i.e. words).

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

[3] The number of matching pairs will end up here. (But see [7].)

[4] Iterate over the words, in alphabetical order.

[5] Reverse the word with flip.

[6] Do we have the reveresed word in the Set? if so, increase the counter.

[7] Print the result, divided by two. As we count both elements in a pair, end the challenge wants us to count the pairs. div is integer division.

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

Running it:

$ ./maximum-pairs ab de ed bc
1

$ ./maximum-pairs aa ba cd ed
0

$ ./maximum-pairs uv qp st vu mn pq
2

Looking good.

With verbose mode:

$ ./maximum-pairs -v ab de ed bc
:   Word ab does not have a reverse
:   Word bc does not have a reverse
: + Word de has a reverse (ed)
: + Word ed has a reverse (de)
1

$ ./maximum-pairs -v aa ba cd ed
: + Word aa has a reverse (aa)
:   Word ba does not have a reverse
:   Word cd does not have a reverse
:   Word ed does not have a reverse
0

$ ./maximum-pairs -v uv qp st vu mn pq
:   Word mn does not have a reverse
: + Word pq has a reverse (qp)
: + Word qp has a reverse (pq)
:   Word st does not have a reverse
: + Word uv has a reverse (vu)
: + Word vu has a reverse (uv)
2

Oops. The second excample does not look good, as «aa» is the reverse of itself. But luckily - or unluckily, depending on your point of view, the div got rid of the 0.5 that normal division would have given us.

But this is still a problem, if we add more palindromes (words where the reverse is the same as the word itself):

$ ./maximum-pairs -v aa bb cc dd
: + Word aa has a reverse (aa)
: + Word bb has a reverse (bb)
: + Word cc has a reverse (cc)
: + Word dd has a reverse (dd)
2

Let us fix that.

File: maximum-pairs-palindrome
#! /usr/bin/env raku

unit sub MAIN (*@words where @words.elems > 0
                  && @words.elems == @words.unique.elems,
               :v(:$verbose));

my $words = @words.Set;
my $count = 0;

for $words.keys.sort -> $word
{
  my $reverse = $word.flip;

  if ($word eq $reverse)                                # [1]
  {
    say ":   Word $word is a palindrome" if $verbose;
  }
  elsif $words{$reverse}                                # [1a]
  {
    $count++;
    say ": + Word $word has a reverse ({$reverse})" if $verbose;
  }
  elsif ($verbose)
  {
    say ":   Word $word does not have a reverse" if $verbose;
  }
}

say $count div 2;

[1] If the word is a palindrome, ignore it. If not, ... [1a].

The palindromic example works now:

$ ./maximum-pairs-palindrome -v aa bb cc dd
:   Word aa is a palindrome
:   Word bb is a palindrome
:   Word cc is a palindrome
:   Word dd is a palindrome
0

Another example, showing all the three word types:

$ ./maximum-pairs-palindrome -v aa ab bb bc cc cd dd dc cb
:   Word aa is a palindrome
:   Word ab does not have a reverse
:   Word bb is a palindrome
: + Word bc has a reverse (cb)
: + Word cb has a reverse (bc)
:   Word cc is a palindrome
: + Word cd has a reverse (dc)
: + Word dc has a reverse (cd)
:   Word dd is a palindrome
2

Challenge #256.2: Merge Strings

You are given two strings, $str1 and $str2.

Write a script to merge the given strings by adding in alternative order starting with the first string. If a string is longer than the other then append the remaining at the end.

Example 1:
Input: $str1 = "abcd", $str2 = "1234"
Output: "a1b2c3d4"
Example 2:
Input: $str1 = "abc", $str2 = "12345"
Output: "a1b2c345"
Example 3:
Input: $str1 = "abcde", $str2 = "123"
Output: "a1b2c3de"

Raku has a Zip function zip and a corresponding operator Z that looks promising. The result is a list of pairs objects, with one value from each input list. But, if the lengths differ, any remaing values will be discarded. E.g.

> say zip("abc".comb, "12345".comb).raku
(("a", "1"), ("b", "2"), ("c", "3")).Seq

> say ("abc".comb Z "12345".comb).raku
(("a", "1"), ("b", "2"), ("c", "3")).Seq

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

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

The roundrobin function takes care of trailing values:

> roundrobin("abc".comb, "12345".comb).raku
(("a", "1"), ("b", "2"), ("c", "3"), ("4",), ("5",)).Seq

> roundrobin("abcde".comb, "123".comb).raku
(("a", "1"), ("b", "2"), ("c", "3"), ("d",), ("e",)).Seq

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

Note that the two single values are lists with one element only (shown by the trailing comma). It is not possible at this point to tell which input list the value came from.

File: merge-strings
#! /usr/bin/env raku

unit sub MAIN ($str1 where $str1.chars > 0,
               $str2 where $str2.chars > 0);

say roundrobin($str1.comb, $str2.comb)>>.join.join; # [1]

[1] The >>.join joins the Pair objects into two character strings, and the .join joins those string together to one single string, ready for printing.

Running it:

$ ./merge-strings abcd 1234
a1b2c3d4

$ ./merge-strings abc 12345
a1b2c345

$ ./merge-strings abcde 123
a1b2c3de

Looking good.

We can do it manually, with gather/take:

File: merge-strings-gather
#! /usr/bin/env raku

unit sub MAIN ($str1 where $str1.chars > 0,
               $str2 where $str2.chars > 0,
	       :v(:$verbose));

my $ms := gather
{
  my @str1 = $str1.comb;
  my @str2 = $str2.comb;

  while (@str1.elems || @str2.elems) # [1]
  {
    if @str1.elems                   # [2a]
    {
      say ":Fetching character '@str1[0]' from string 1" if $verbose;
      take @str1.shift;              # [2]
    }
    if @str2.elems                   # [3a]
    {
      say ":Fetching character '@str2[0]' from string 2" if $verbose;
      take @str2.shift;              # [3]
    }
  }	
}

say $ms.join;

[1,2,3] As long as there are more values in the input strings [1], take one from the first [2] if there are more in that one [2a], and take one from the second [3] if there are more in that one [3a].

See my Raku Gather, I Take article or docs.raku.org/language/control#gather/take for more information about gather/take.

Running it gives the expected result, shown here with verbose mode:

$ ./merge-strings-gather -v abcd 1234
:Fetching character 'a' from string 1
:Fetching character '1' from string 2
:Fetching character 'b' from string 1
:Fetching character '2' from string 2
:Fetching character 'c' from string 1
:Fetching character '3' from string 2
:Fetching character 'd' from string 1
:Fetching character '4' from string 2
a1b2c3d4

$ ./merge-strings-gather -v abc 12345
:Fetching character 'a' from string 1
:Fetching character '1' from string 2
:Fetching character 'b' from string 1
:Fetching character '2' from string 2
:Fetching character 'c' from string 1
:Fetching character '3' from string 2
:Fetching character '4' from string 2
:Fetching character '5' from string 2
a1b2c345

$ ./merge-strings-gather -v abcde 123
:Fetching character 'a' from string 1
:Fetching character '1' from string 2
:Fetching character 'b' from string 1
:Fetching character '2' from string 2
:Fetching character 'c' from string 1
:Fetching character '3' from string 2
:Fetching character 'd' from string 1
:Fetching character 'e' from string 1
a1b2c3de

And that's it.