String Goal
with Raku

by Arne Sommer

String Goal with Raku

[388] Published 15. March 2026.

This is my response to The Weekly Challenge #364.

#364.1 Decrypt String You are given a string formed by digits and ‘#'.

Write a script to map the given string to English lowercase characters following the given rules.
  • Characters 'a' to 'i' are represented by '1' to '9' respectively
  • Characters 'j' to 'z' are represented by '10#' to '26#' respectively
Example 1:
Input: $str = "10#11#12"
Output: "jkab"

10# -> j
11# -> k
1   -> a
2   -> b
Example 2:
Input: $str = "1326#"
Output: "acz"

1   -> a
3   -> c
26# -> z
Example 3:
Input: $str = "25#24#123"
Output: "yxabc"

25# -> y
24# -> x
1   -> a
2   -> b
3   -> c
Example 4:
Input: $str = "20#5"
Output: "te"

20# -> t
5   -> e
Example 5:
Input: $str = "1910#26#"
Output: "aijz"

1   -> a
9   -> i
10# -> j
26# -> z

This task is suitable for gather/take, where we use gather to collect the parts (the numeric values), the first - and hardest - part of this task.

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

File: decrypt-string
#! /usr/bin/env raku

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

my $tokens := gather                                                  # [2]
{
  my @chars = $string.comb;                                           # [3]

  while @chars.elems                                                  # [4]
  {
    if @chars.elems > 2 && @chars[2] eq '#'                           # [5]
    {
      say ": '{ @chars.join }' -> take '{ @chars[0..2].join }'" if $verbose;
      take (@chars.shift ~ @chars.shift ~ @chars.shift).substr(0,2);  # [5a]
    }
    else                                                              # [6]
    {
      say ": '{ @chars.join }' -> take '@chars[0]'" if $verbose;
      take @chars.shift;                                              # [6a]
    }
  }
}

say $tokens.map({ .&decrypt }).join;                                  # [7]

sub decrypt ($key)                                                    # [8]
{
  constant base = 'a'.ord -1;                                         # [9]

  return (base + $key).chr;                                           # [10]
}

[1] A string with at least 1 character.

[2] Collect the numeric values (here called tokens).

[3] Split the string into an array of individual characters.

[4] As long as we have unprocessed input.

[5] Do we have at least three characters left, and the third one is #? If so, get the three characters from the left of the array (with shift) and return the first two joined together (with substr and take [5a]).

[6] If not, get the first character and return it [6a].

[7] Apply the «decrypt» procedure on each element, join the resulting values to a string and print that.

[8] The procedure doing the decryption.

[9] The ascii value of 'a' (courtesy of ord), less one. To be used as offset.

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

[10] Convert the ascii value of the alphabetic number (e.g. 1 => a, 2 => b, and so on) back to a character (with chr) and return it.

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

Running it:

$ ./decrypt-string "10#11#12"
jkab

$ ./decrypt-string "1326#"
acz

$ ./decrypt-string "25#24#123"
yxabc

$ ./decrypt-string "20#5"
te

$ ./decrypt-string "1910#26#"
aijz

Looking good.

With verbose mode:

$ ./decrypt-string -v "10#11#12"
: '10#11#12' -> take '10#'
: '11#12' -> take '11#'
: '12' -> take '1'
: '2' -> take '2'
jkab

$ ./decrypt-string -v "1326#"
: '1326#' -> take '1'
: '326#' -> take '3'
: '26#' -> take '26#'
acz

$ ./decrypt-string -v "25#24#123"
: '25#24#123' -> take '25#'
: '24#123' -> take '24#'
: '123' -> take '1'
: '23' -> take '2'
: '3' -> take '3'
yxabc

$ ./decrypt-string -v "20#5"
: '20#5' -> take '20#'
: '5' -> take '5'
te

$ ./decrypt-string -v "1910#26#"
: '1910#26#' -> take '1'
: '910#26#' -> take '9'
: '10#26#' -> take '10#'
: '26#' -> take '26#'
aijz

#364.2 Goal Parser You are given a string, $str.

Write a script to interpret the given string using Goal Parser.

The Goal Parser interprets “G” as the string “G”, “()” as the string “o”, and “(al)” as the string “al”. The interpreted strings are then concatenated in the original order.

Example 1:
Input: $str = "G()(al)"
Output: "Goal"

G    -> "G"
()   -> "o"
(al) -> "al"
Example 2:
Input: $str = "G()()()()(al)"
Output: "Gooooal"

G       -> "G"
four () -> "oooo"
(al)    -> "al"
Example 3:
Input: $str = "(al)G(al)()()"
Output: "alGaloo"

(al) -> "al"
G    -> "G"
(al) -> "al"
()   -> "o"
()   -> "o"
Example 4:
Input: $str = "()G()G"
Output: "oGoG"

() -> "o"
G  -> "G"
() -> "o"
G  -> "G"
Example 5:
Input: $str = "(al)(al)G()()"
Output: "alalGoo"

(al) -> "al"
(al) -> "al"
G    -> "G"
()   -> "o"
()   -> "o"
File: goal-parser
#! /usr/bin/env raku

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

my $tokens := gather                                               # [2]
{
  while $string.chars                                              # [3]
  {
    if $string ~~ /^([ "G" | "\(\)" | "\(al\)" ]) (.*)/            # [4]
    {
      say ": '$string' -> '$0' + '$1'" if $verbose;
      given $0.Str                                                  # [5]
      {
        when "G"    { take "G" }                                    # [5G]
        when "()"   { take "o" }                                    # [5o]
        when "(al)" { take "al" }                                   # [5al]
      }
      $string = $1.Str // "";                                       # [6]
    }
    else
    {
      die "Illegal input; not a goal";                              # [7]
    }
  }
}
say $tokens.join;                                                   # [8]

[1] Ensure a string with at least 1 character. The is_copy adverb makes it possible to change the value later on (in [6]).

See docs.raku.org/type/Parameter#method_copy for more information about is copy.

[2] Collect the tokens with gather.

[3] As long as we have more characters to process.

[4] This regexp looks for the three "characters".

[5] Use given/when to check the regexp match, and take to return the translation.

See docs.raku.org/syntax/default when for more information about given/when/default.

[6] Update the string (by removing the part we just translated), ready for the next iteration.

[7] No regexp match? Report the error and terminate.

[8] Print the result, as a string.

Running it:

$ ./goal-parser "G()(al)"
Goal

$ ./goal-parser "G()()()()(al)"
Gooooal

$ ./goal-parser "(al)G(al)()()"
alGaloo

$ ./goal-parser "()G()G"
oGoG

$ ./goal-parser "(al)(al)G()()"
alalGoo

Looking good.

With verbose mode:

$ ./goal-parser -v "G()(al)"
: 'G()(al)' -> 'G' + '()(al)'
: '()(al)' -> '()' + '(al)'
: '(al)' -> '(al)' + ''
Goal

$ ./goal-parser -v "G()()()()(al)"
: 'G()()()()(al)' -> 'G' + '()()()()(al)'
: '()()()()(al)' -> '()' + '()()()(al)'
: '()()()(al)' -> '()' + '()()(al)'
: '()()(al)' -> '()' + '()(al)'
: '()(al)' -> '()' + '(al)'
: '(al)' -> '(al)' + ''
Gooooal

$ ./goal-parser -v "(al)G(al)()()"
: '(al)G(al)()()' -> '(al)' + 'G(al)()()'
: 'G(al)()()' -> 'G' + '(al)()()'
: '(al)()()' -> '(al)' + '()()'
: '()()' -> '()' + '()'
: '()' -> '()' + ''
alGaloo

$ ./goal-parser -v "()G()G"
: '()G()G' -> '()' + 'G()G'
: 'G()G' -> 'G' + '()G'
: '()G' -> '()' + 'G'
: 'G' -> 'G' + ''
oGoG

$ ./goal-parser -v "(al)(al)G()()"
: '(al)(al)G()()' -> '(al)' + '(al)G()()'
: '(al)G()()' -> '(al)' + 'G()()'
: 'G()()' -> 'G' + '()()'
: '()()' -> '()' + '()'
: '()' -> '()' + ''
alalGoo

The else part is there to take care of non-goal input, like e.g.

$ ./goal-parser -v "G()(al)!"
: 'G()(al)!' -> 'G' + '()(al)!'
: '()(al)!' -> '()' + '(al)!'
: '(al)!' -> '(al)' + '!'
Illegal input; not a goal
  in block  at ./goal-parser line 22
  in sub MAIN at ./goal-parser line 26
  in block <unit> at ./goal-parser line 1

Without this else block, the program would go on in an infinite loop.

An alternative could be to let non-goal characters pass through unchanged into the output.

File: goal-parser-allow
#! /usr/bin/env raku

unit sub MAIN ($string is copy where $string.chars > 0, :v(:$verbose));

my $tokens := gather
{
  while $string.chars
  {
    if $string ~~ /^([ "G" | "\(\)" | "\(al\)" ] | . ) (.*)/  # [1]
    {
      say ": '$string' -> '$0' + '$1'" if $verbose;
      given $0.Str
      {
        when "G"    { take "G" }
        when "()"   { take "o" }
        when "(al)" { take "al" }
	default     { take $_ }                               # [2]
      }
      $string = $1.Str // "";
    }                                                         # [3]
  }
}
say $tokens.join;

[1] Note the single period, that matches a single character.

[2] We use default to catch the non-goal match.

[3] The else block has gone.

Running it:

$ ./goal-parser-allow -v "G()(al)!"
: 'G()(al)!' -> 'G' + '()(al)!'
: '()(al)!' -> '()' + '(al)!'
: '(al)!' -> '(al)' + '!'
: '!' -> '!' + ''
Goal!

$ ./goal-parser-allow -v "foo()bar"
: 'foo()bar' -> 'f' + 'oo()bar'
: 'oo()bar' -> 'o' + 'o()bar'
: 'o()bar' -> 'o' + '()bar'
: '()bar' -> '()' + 'bar'
: 'bar' -> 'b' + 'ar'
: 'ar' -> 'a' + 'r'
: 'r' -> 'r' + ''
fooobar

The fact that «G» is translated to «G» is a strong indication that this version of the program gets it wrong (as the default block makes that rule redundant). So stick with the first version.

And that's it.