Reverse Broken
with Raku

by Arne Sommer

Reverse Broken with Raku

[334] Published 19. March 2025.

This is my response to The Weekly Challenge #313.

Challenge #313.1: Broken Keys

You have a broken keyboard which sometimes type a character more than once.

You are given a string and actual typed string.

Write a script to find out if the actual typed string is meant for the given string.

Example 1:
Input: $name = "perl", $typed = "perrrl"
Output: true

Here "r" is pressed 3 times instead of 1 time.
Example 2:
Input: $name = "raku", $typed = "rrakuuuu"
Output: true
Example 3:
Input: $name = "python", $typed = "perl"
Output: false
Example 4:
Input: $name = "coffeescript", $typed = "cofffeescccript"
Output: true

I have chosen to use the variable names $in for the input, and $out for whatever the typewriter chooses to print.

File: broken-keys
#! /usr/bin/env raku

unit sub MAIN (:i(:$in)  where $in.chars > 0,      # [1]
               :o(:$out) where $out.chars > 0,     # [2]
	       :v(:$verbose));

sub grouped ($str)                                 # [3]
{
  my @parts = gather                               # [4]
  {
    my $curr;                                      # [5]
    my $count = 0;                                 # [6]
    
    for $str.comb -> $char                         # [7]
    {
      if $count && $curr ne $char                  # [8]
      {
        take $curr x $count;                       # [9]
	$count = 0;                                # [10]
      }
      $curr = $char;                               # [11]
      $count++;                                    # [12]
    }
    take $curr x $count;                           # [13]
  } 

  say ": Grouped '$str' as { @parts.raku }" if $verbose;
  return @parts;                                   # [14]
}

my @in  = grouped($in);                            # [15]
my @out = grouped($out);                           # [16]

my @combined = roundrobin(@in, @out);              # [17]

say ": Zipped: {@combined.raku }" if $verbose;

my $status = True;                                 # [18]

for @combined -> @pair                             # [19]
{
  if @pair.elems == 1                              # [20]
  {
    $status = False;                               # [20a]
    say ": Trailing character: { @pair.flat }" if $verbose;
    last;                                          # [20b]
  }

  if @pair[0].substr(0,1) ne @pair[1].substr(0,1)  # [21]
  {
    $status = False;                               # [21a]
    say ": Different starting characters: { @pair[0].substr(0,1) } \
      <> { @pair[1].substr(0,1) }" if $verbose;

    last;                                          # [21b]
  }

  if @pair[0].chars > @pair[1].chars               # [22]
  {
    $status = False;                               # [22a]
    say ": Not enough '{ @pair[0].substr(0,1) }' characters. Got \
      { @pair[1].chars } requires { @pair[0].chars } " if $verbose;  

    last;                                          # [22b]
  }

  say ": Ok ('{ @pair[0] }' vs '{ @pair[1] }')" if $verbose;

}

say $status;                                       # [23]

[1] The input.

[2] The (possibly legal) output.

[3] The procedure splitting the string into chunks of identical characters. We will use this to compare the chunks of the input and output one by one later on.

[4] This task is eminently suitable for gather/take. We start here by collecting (or gathering, to be pedantic) the values delivered (or taken, again pedantic) in [9] and [13]. This is kind of like putting a glass under a leaking faucet to cellect it all.

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

[5] The current character, initially none.

[6] The number of times we have encountered that character, also none initially.

[7] Iterate over each character, courtesy of comb on the string.

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

[8] The same character as the previous one? Note the check on $count to avoid setting it off on the very first character.

[9] Not the same character? Then return (with take) the character repeated as many times as it occured in the original string in this sequence. Note the use of the string repetition operator x to get the correct length.

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

[10] Reset the number of times.

[11] Set the current character to the new one (or the same).

[12] Add the character to the count.

[13] Handle the last sequence of a character.

[14] Return the result.

[15] Split the input string,

[16] and the output string.

[17] Combine the two arrays with roundrobin, a zip operator that allows for either array to be shorter than the other without failing. The result is an array consisting of subarrays with two elements each, one from the input array and one from the output array.

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

[18] We assume that this will work out, thus True. Falsification is the name of the game. Or: failure to falsify means success.

[19] Iterate over the array, giving us the subarrays - one at a time.

[20] If the current subarray has one element only, regardless of which one of them (input or output) that is missing, we certainly do not have a matching pair of letters. Set the status to False [20a] and exit the loop (with last) [20b].

[21] If the first characters differ, we have falsification.

[22] We have the same first character, courtesy of [21]. Now we check that the output has enough of them to match the input (i.e. the same number or more).

[23] Return the status.

The [20-22] parts could have been written as a single if condition, if (pun intended) we had skipped the verbose output.

Running it:

$ ./broken-keys -i=perl -o=perrrl
True

3$ ./broken-keys -i=raku -o=rrakuuuu
True

$ ./broken-keys -i=python -o=perl
False

$ ./broken-keys -i=coffeescript -o=cofffeescccript
True

Looking good.

With verbose mode:

$ ./broken-keys -v -i=perl -o=perrrl
: Grouped 'perl' as ["p", "e", "r", "l"]
: Grouped 'perrrl' as ["p", "e", "rrr", "l"]
: Zipped: [("p", "p"), ("e", "e"), ("r", "rrr"), ("l", "l")]
: Ok ('p' vs 'p')
: Ok ('e' vs 'e')
: Ok ('r' vs 'rrr')
: Ok ('l' vs 'l')
True

$ ./broken-keys -v -i=raku -o=rrakuuuu
: Grouped 'raku' as ["r", "a", "k", "u"]
: Grouped 'rrakuuuu' as ["rr", "a", "k", "uuuu"]
: Zipped: [("r", "rr"), ("a", "a"), ("k", "k"), ("u", "uuuu")]
: Ok ('r' vs 'rr')
: Ok ('a' vs 'a')
: Ok ('k' vs 'k')
: Ok ('u' vs 'uuuu')
True

$ ./broken-keys -v -i=python -o=perl
: Grouped 'python' as ["p", "y", "t", "h", "o", "n"]
: Grouped 'perl' as ["p", "e", "r", "l"]
: Zipped: [("p", "p"), ("y", "e"), ("t", "r"), ("h", "l"), ("o",), ("n",)]
: Ok ('p' vs 'p')
: Different starting characters: y <> e
False

$ ./broken-keys -v -i=coffeescript -o=cofffeescccript
: Grouped 'coffeescript' as ["c", "o", "ff", "ee", "s", "c", "r", "i", "p", \
   "t"]
: Grouped 'cofffeescccript' as ["c", "o", "fff", "ee", "s", "ccc", "r", \
   "i", "p", "t"]
: Zipped: [("c", "c"), ("o", "o"), ("ff", "fff"), ("ee", "ee"), ("s", "s"), \
      ("c", "ccc"), ("r", "r"), ("i", "i"), ("p", "p"), ("t", "t")]
: Ok ('c' vs 'c')
: Ok ('o' vs 'o')
: Ok ('ff' vs 'fff')
: Ok ('ee' vs 'ee')
: Ok ('s' vs 's')
: Ok ('c' vs 'ccc')
: Ok ('r' vs 'r')
: Ok ('i' vs 'i')
: Ok ('p' vs 'p')
: Ok ('t' vs 't')
True

Challenge #313.2: Reverse Letters

You are given a string.

Write a script to reverse only the alphabetic characters in the string.

Example 1:
Input: $str = "p-er?l"
Output: "l-re?p"
Example 2:
Input: $str = "wee-k!L-y"
Output: "yLk-e!e-w"
Example 3:
Input: $str = "_c-!h_all-en!g_e"
Output: "_e-!g_nel-la!h_c"

File: reverse-letters
#! /usr/bin/env raku

unit sub MAIN ($str where $str.chars > 0,               # [1]
               :v(:$verbose));

my @str     = $str.comb;                                # [2]
my @letters = @str.grep( /<:L>/ );                      # [3]

if $verbose
{
  say ": Letters: { @letters.join(",") }";
  say ": Reverse: { @letters.reverse.join(",") }";
}

@str.map({ /<:L>/ ?? @letters.pop !! $_ }).join.say;    # [4]

[1] Ensure at least one character in the string.

[2] Split the string into a list of separate characters with comb.

[3] An array with the letters only, courtesy of grep and the Unicode letter character class <:L>.

See docs.raku.org/language/regexes for more information about Rexeces and the different character classes available.

[4] Apply map to map (or translate) each character to either a letter (from the end of the letters set up in [3] with pop) or itself, using a ternary ??!!. Then we join the characters into a single string and print it.

See docs.raku.org/language/operators#index-entry-operator_ternary for more information about the ternary operator ?? / !!.

Running it, using single quotes to protect the string from shell interference:

$ ./reverse-letters 'p-er?l'
l-re?p

$ ./reverse-letters 'wee-k!L-y'
yLk-e!e-w

$ ./reverse-letters '_c-!h_all-en!g_e'
_e-!g_nel-la!h_c

Looking good.

With verbose mode:

$ ./reverse-letters -v  'p-er?l'
: Letters: p,e,r,l
: Reverse: l,r,e,p
l-re?p

$ ./reverse-letters -v 'wee-k!L-y'
: Letters: w,e,e,k,L,y
: Reverse: y,L,k,e,e,w
yLk-e!e-w

$ ./reverse-letters -v '_c-!h_all-en!g_e'
: Letters: c,h,a,l,l,e,n,g,e
: Reverse: e,g,n,e,l,l,a,h,c
_e-!g_nel-la!h_c

And that's it.