Letters and Groups
with Raku

by Arne Sommer

Letters and Groups with Raku

[232] Published 16. April 2023.

This is my response to The Weekly Challenge #212.

Challenge #212.1: Jumping Letters

You are given a word having alphabetic characters only, and a list of positive integers of the same length.

Write a script to print the new word generated after jumping forward each letter in the given word by the integer in the list. The given list would have exactly the number as the total alphabets in the given word.

Example 1:
Input: $word = 'Perl' and @jump = (2,22,19,9)
Output: Raku

'P' jumps 2 place forward and becomes 'R'.
'e' jumps 22 place forward and becomes 'a'. (jump is cyclic i.e. after
                                             'z' you go back to 'a')
'r' jumps 19 place forward and becomes 'k'.
'l' jumps 9 place forward and becomes 'u'.
Example 2:
Input: $word = 'Raku' and @jump = (24,4,7,17)
Output: 'Perl'
File: jumping-letters
#! /usr/bin/env raku

unit sub MAIN ($word where $word ~~ /^<[a..zA..Z]>+$/,         # [1]
               *@jump where @jump.all ~~ UInt                  # [2]
                  && @jump.all < 26                            # [2a]
	          && @jump.elems == $word.chars);              # [2b]

for $word.comb -> $letter                                      # [3]
{
  my $is-lc = $letter eq any('a' ... 'z');                     # [4]
  my $jump  = @jump.shift;                                     # [5]
  my $new   = chr($letter.ord + $jump);                        # [6]

  $new = chr($new.ord - 26) if  $is-lc && $new.ord > 'z'.ord;  # [7]
  $new = chr($new.ord - 26) if !$is-lc && $new.ord > 'Z'.ord;  # [8]

  print $new;                                                  # [9]
}

say "";                                                        # [10]

[1] The first argument is the word, which must consist of ascii lowercase and uppercase letters only, and at least one.

[2] The rest is a list of unsigned integers (the UInt). All of them must be lower than 26 (the length of the ascii alphabet), as adding 26 would rountrip to the same letter [2a]. And finally we insist on one number for each letter in the word [2b].

[3] Iterate over the letters in the word.

[4] Do we have a lowercase letter? (If not, it is uppercase courtesy of [1]).

[5] Get the current jump value.

[6] Add the jump value to the character codepoint (ord) and convert that value back to a character (chr).

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

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

[7] If we have a lowercase letter and the letter is higher than 'z' we have to roundtrip it back inside the lowercase range by subtracting 26 from the codepoint.

[8] As above, but for an uppercase letter. Here we check for 'Z' instead of 'z'.

[9] Print the character.

[10] Print a trailing newline, after the new word.

Running it:

$ ./jumping-letters Perl 2 22 19 9
Raku

$ ./jumping-letters Raku 24 4 7 17
Perl

Looking good.

Challenge #212.2: Rearrange Groups

You are given a list of integers and group size greater than zero.

Write a script to split the list into equal groups of the given size where integers are in sequential order. If it can’t be done then print -1.

Example 1:
Input: @list = (1,2,3,5,1,2,7,6,3) and $size = 3
Output: (1,2,3), (1,2,3), (5,6,7)
Example 2:
Input: @list = (1,2,3) and $size = 2
Output: -1
Example 3:
Input: @list = (1,2,4,3,5,3) and $size = 3
Output: (1,2,3), (3,4,5)
Example 4:
Input: @list = (1,5,2,6,4,7) and $size = 3
Output: -1

File: rearrange-group
#! /usr/bin/env raku

multi MAIN (UInt $size where $size >= 1,            # [1]
           *@list where @list.elems >= 2            # [1a]
              && @list.all ~~ Int                   # [1b]
              && @list.elems %% $size,              # [1c]
           :v(:$verbose))
{
  say ": size: $size" if $verbose;

  my @sorted = @list>>.Int.sort;                    # [2]

  say ": Sorted source: @sorted[]" if $verbose;
  
  my @res;                                          # [3]
  
  for 1 .. @list.elems / $size -> $group            # [4]
  {
    my @group = @sorted.shift;                      # [5]

    say ": New group $group starting with @group[0]" if $verbose;
    
    while @group.elems < $size                      # [6]
    {
      my $target = @group.tail + 1;                 # [7]
      my $index  = @sorted.first($target, :k);      # [8]
      
      if defined $index                             # [9]
      {
        say ": Found target $target at index $index" if $verbose;
	
        @group.append: @sorted.splice($index,1);    # [9a]

        say ": Group now: @group[] (Rest: @sorted[])" if $verbose;
      }
      else                                          # [10]
      {
        say ": Did not find target $target" if $verbose;
        say '-1';                                   # [10a]
	exit;                                       # [10b]
      }
    }
			 
    @res.push: @group;                              # [11]
    say ": Added group to result: @group[]" if $verbose;
  }

  say @res>>.join(",").map({ "($_)" }).join(", ");  # [12]
}

multi MAIN (*@slurp, :v(:$verbose))                 # [13]
{
  say '-1';                                         # [13a]
}

[1] I have chosen to use multiple dispatch, and this is the first version of MAIN. The first argument is the size, which must be positive. The second is s slurpy array with at least two elements [1a], they must all be integers [1b] (including negative values), and it must be possible to divide the list by the size [1c]. See [13] for the second version of MAIN.

[2] Sort the values, with the lowest first. And coerce them to integers, instead of the default IntStr we get as a result of using the command line.

[3] The result will end up here.

[4] Iterate over the number of groups we are going to (try to) create.

[5] Get the first value, the currently lowest unused value.

[6] As long as we have not filled up the current group.

[7] The target value to look for, one more than the last one already in the group (with tail).

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

[8] Get the index of the target, with first. Note the :k adverb, used to get the index instead of the actual value.

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

[9] Did we find it? (Note the use of defined, as 0 is a perfectly valid index value). If so, use splice to extract and remove that one character from the array and add it to the group. Note the use of append, as splice gives a list - even when we only get one value as here - instead of push, so that we keep the group as a single array.

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

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

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

[10] If we did not find the value, print «-1» [10a] and exit [10b].

[11] Add the group to the result array.

[12] Print the two dimentional array, just as specified in the challenge.

[13] The second version of MAIN, which is used if the first one does not match the arguments.

Running it:

$ ./rearrange-group 3 1 2 3 5 1 2 7 6 3
(1,2,3), (1,2,3), (5,6,7)

$ ./rearrange-group 2 1 2 3
-1

$ ./rearrange-group 3 1 2 4 3 5 3
(1,2,3), (3,4,5)

$ ./rearrange-group 3 1 5 2 6 4 7
-1

Looking good.

With verbose mode:

$ ./rearrange-group -v 3 1 2 3 5 1 2 7 6 3
: size: 3
: Sorted source: 1 1 2 2 3 3 5 6 7
: New group 1 starting with 1
: Found target 2 at index 1
: Group now: 1 2 (Rest: 1 2 3 3 5 6 7)
: Found target 3 at index 2
: Group now: 1 2 3 (Rest: 1 2 3 5 6 7)
: Added group to result: 1 2 3
: New group 2 starting with 1
: Found target 2 at index 0
: Group now: 1 2 (Rest: 3 5 6 7)
: Found target 3 at index 0
: Group now: 1 2 3 (Rest: 5 6 7)
: Added group to result: 1 2 3
: New group 3 starting with 5
: Found target 6 at index 0
: Group now: 5 6 (Rest: 7)
: Found target 7 at index 0
: Group now: 5 6 7 (Rest: )
: Added group to result: 5 6 7
(1,2,3), (1,2,3), (5,6,7)

$ ./rearrange-group -v 2 1 2 3
-1

$ ./rearrange-group -v 3 1 2 4 3 5 3
: size: 3
: Sorted source: 1 2 3 3 4 5
: New group 1 starting with 1
: Found target 2 at index 0
: Group now: 1 2 (Rest: 3 3 4 5)
: Found target 3 at index 0
: Group now: 1 2 3 (Rest: 3 4 5)
: Added group to result: 1 2 3
: New group 2 starting with 3
: Found target 4 at index 0
: Group now: 3 4 (Rest: 5)
: Found target 5 at index 0
: Group now: 3 4 5 (Rest: )
: Added group to result: 3 4 5
(1,2,3), (3,4,5)

$ ./rearrange-group -v 3 1 5 2 6 4 7
: size: 3
: Sorted source: 1 2 4 5 6 7
: New group 1 starting with 1
: Found target 2 at index 0
: Group now: 1 2 (Rest: 4 5 6 7)
: Did not find target 3
-1

Note that the second example (-1) was catched by the second MULTI, whilst the fourth was detected by [10].

And that's it.