Special Account
with Raku

by Arne Sommer

Special Account with Raku

[229] Published 24. march 2023.

This is my response to The Weekly Challenge #209.

Challenge #209.1: Special Bit Characters

You are given an array of binary bits that ends with 0.

Valid sequences in the bit string are:

[0] -decodes-to-> "a"
[1, 0] -> "b"
[1, 1] -> "c"
Write a script to print 1 if the last character is an “a” otherwise print 0.

Example 1:
Input: @bits = (1, 0, 0)
Output: 1

The given array bits can be decoded as 2-bits character (10) followed by
1-bit character (0).
Example 2:
Input: @bits = (1, 1, 1, 0)
Output: 0

Possible decode can be 2-bits character (11) followed by 2-bits character
(10) i.e. the last character is not 1-bit character.
File: special-bit-characters
#! /usr/bin/env raku

unit sub MAIN (*@bits where @bits.elems > 0         # [1]
                         && all(@bits) eq any(0,1)  # [1a]
                         && @bits[*-1] == 0,        # [1b]
               :v($verbose));

my $string = "";                                    # [2]

while (@bits.elems)                                 # [3]
{
  my $first = @bits.shift;                          # [4]

  if $first == 0                                    # [5]
  {
    $string ~= 'a';                                 # [5a]
  }
  elsif (@bits.elems)                               # [6]
  {
    my $second = @bits.shift;                       # [6a]
    $string ~= $second == 0 ?? 'b' !! 'c';          # [6b]
  }
  else                                              # [7]
  {
    $string ~= "ERROR";                             # [7a]
  }
}

say ":String: $string" if $verbose;

say + $string.ends-with('a');                       # [8]

[1] A slurpy array with at least one element, the values must be either «0» or «1» [1a] and the last value (ggiven with the [*-1] index) must be «0» [1b].

[2] The resulting string, which the challenge does not actually request, will end up here.

[3] As long as we have more values (bits) to parse.

[4] Get the next value (the first unparsed).

[5] Is it zero? In that case add an «a» to the string [5a].

[6] If not (i.e. it is «1»), and we have at least one more value, get that value [6a]. If the second value is zero, we get the letter «b» and if it is one, we get «c» [6b]. (~ is the string concatenation operator, and ~= assigns the new value back to the variable on the left, in the same way as e.g. $a += 2 would for numeric values.)

[7] If the first value is one, and that one is the last one, we have an error. Here I have just added the error message to the string. Note that the rule given in [1b] makes this code unreachable. A die is probably a better choice here...

[8] Print «1» if the last character in the string is an «a». We use the aptly named ends-with to look it up. The result is a Boolean value, and we coerce that to a number (ie. True => 1, False => 0) with the numeric coercion prefix +.

See docs.raku.org/routine/ends-with for more information about the ends-with function.

See docs.raku.org/routine/+ for more information about the Numeric Coercion Prefix Operator +.

Running it:

$ ./special-bit-characters 1 0 0
1

$ ./special-bit-characters 1 1 1 0
0

Looking good.

Verbose mode may help convince you that the result is correct for the right reasons:

$ ./special-bit-characters -v 1 0 0
:String: ba
1

$ ./special-bit-characters -v 1 1 1 0
:String: cb
0

Illegal input, i.e. the last value is not zero, is catched:

$ ./special-bit-characters 1 0 0 1
Usage:
  ./special-bit-characters [-v[=Any]] [<bits> ...]

Challenge #209.2: Merge Account

You are given an array of accounts i.e. name with list of email addresses.

Write a script to merge the accounts where possible. The accounts can only be merged if they have at least one email address in common.

Example 1:
Input: @accounts = [ ["A", "a1@a.com", "a2@a.com"],
                     ["B", "b1@b.com"],
                     ["A", "a3@a.com", "a1@a.com"] ]
                   ]

Output: [ ["A", "a1@a.com", "a2@a.com", "a3@a.com"],
          ["B", "b1@b.com"] ]
Example 2:
Input: @accounts = [ ["A", "a1@a.com", "a2@a.com"],
                     ["B", "b1@b.com"],
                     ["A", "a3@a.com"],
                     ["B", "b2@b.com", "b1@b.com"] ]

Output: [ ["A", "a1@a.com", "a2@a.com"],
          ["A", "a3@a.com"],
          ["B", "b1@b.com", "b2@b.com"] ]

This challenge can be quite difficult.

  • It does not specify that the names of the accounts to merge must be the same, but it is reasonable to assume so
  • Several accounts can have the same name, even if we cannot merge them (as shown for «A» in the second example) so we cannot use a normal hash to store the email addresses - with the account names (in this case «A») as keys.
  • How many times do we need to do the check? If we have «A a b c«, «A d e f» and «A a f g» (where the A is the account name, and the lowercase letters are email addresses) we should merge them into one single account - but this requires two passes. It is possible to construct examples that would require even more passes... But let us assume that one pass is enough
File: merge-account
#! /usr/bin/env raku

unit sub MAIN (:v($verbose));

my @accounts1 = [ ["A", "a1@a.com", "a2@a.com"],  # [1]
                  ["B", "b1@b.com"],
                  ["A", "a3@a.com", "a1@a.com"]
                ];

say "Example 1:";                                 # [1a]
merge-accounts(@accounts1);                       # [1a]

my @accounts2 = [ ["A", "a1@a.com", "a2@a.com"],  # [2]
                  ["B", "b1@b.com"],
                  ["A", "a3@a.com"],
                  ["B", "b2@b.com", "b1@b.com"]
                ];

say "\nExample2:";                                # [2a]
merge-accounts(@accounts2);                       # [2a]

sub merge-accounts (@accounts)                    # [3]
{
  my %accounts;                                   # [4]

ACC:
  for @accounts -> @account                       # [5]
  {
    my $key = @account.shift;                     # [6]
    my @email = @account;                         # [6a]

    if %accounts{$key}                            # [7]
    {
      for @(%accounts{$key}) -> @emails           # [8]
      {
        if any(@emails) eq any(@email)            # [9]
        {
	  say ":Append $key emails: @email[] (to @emails[])" if $verbose;
          @emails.append: @email;                 # [10]
          next ACC;                               # [11]
        }
      }
    }

    say ":Add $key emails: @email[]" if $verbose;

    %accounts{$key}.push: @email;                 # [12]
  }

  say "[";                                        # [13]
  for sort keys %accounts -> $key                 # [14]
  {
    for @(%accounts{$key}) -> @emails             # [15]
    {
      say "  [\"$key\", ", join(", ", @emails.unique.map({ "\"$_\""}) ), "],";
    }                                             # [16]
  }
  say "]";                                        # [13a]
}

You may want to run the program with verbose mode (as shown later on), and follow the verbose output lines of code to follow the program flow, whilst going through the following discussion.

[1] The first example.

[2] The second example.

[3] The procedure doing the merging and printing.

[4] The merged data will end up here.

[5] Iterate over the rows of accounts in the input.

[6] The first value is the account name (or key), and the rest of the values are email addresses; one or more [6a].

[7] Have we encountered an account with this name before?

[8] If so, iterate over the arrays of email addresses that we have saved.

[9] If the new account and the one in the loop has (at least) one common address,

[10] add the new account email addresses to this one, with append so that we add them to the current array.

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

[11] Skip checking the rest of the rows in the loop [8] and go to the next value in the input array [5]. This will skip the push in [12].

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

[12] No match if we get here, so add it as a new array. Note that pushing to a hash value turns the value into an array (and any existing scalar value already there will silently go away (but not in REPL mode)).

Then the output, or presentation, of the resulting data structure:

[13] Print the outer brackets [13a].

[14] Iterate over the account names, in sorted order.

[15] Iterate over the arrays of email addresses for each one.

[16] Print the name and email addresses, quoted and comma separated. Note the unique to get rid of duplicates (the common email addresses).

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

Running it:

$ ./merge-account
Example 1:
[
  ["A", "a1@a.com", "a2@a.com", "a3@a.com"],
  ["B", "b1@b.com"],
]

Example2:
[
  ["A", "a1@a.com", "a2@a.com"],
  ["A", "a3@a.com"],
  ["B", "b1@b.com", "b2@b.com"],
]

The trailing comma on the last account line is redundant, but Raku (and Perl) does not mind. (We may not actually be required to actually print the result like this, just returning the data structure. But printing is certainly required to show what we got, so...

Running it with verbose mode:

$ ./merge-account -v
Example 1:
:Add A emails: a1@a.com a2@a.com
:Add B emails: b1@b.com
:Append A emails: a3@a.com a1@a.com (to a1@a.com a2@a.com)
[
  ["A", "a1@a.com", "a2@a.com", "a3@a.com"],
  ["B", "b1@b.com"],
]

Example2:
:Add A emails: a1@a.com a2@a.com
:Add B emails: b1@b.com
:Add A emails: a3@a.com
:Append B emails: b2@b.com b1@b.com (to b1@b.com)
[
  ["A", "a1@a.com", "a2@a.com"],
  ["A", "a3@a.com"],
  ["B", "b1@b.com", "b2@b.com"],
]

And that's it.