Ascending Duplicates
with Raku

by Arne Sommer

Ascending Duplicates with Raku

[364] Published 27. September 2025.

This is my response to The Weekly Challenge #340.

Challenge #340.1: Duplicate Removals

You are given a string, $str, consisting of lowercase English letters.

Write a script to return the final string after all duplicate removals have been made. Repeat duplicate removals on the given string until we no longer can.

Example 1:
Input: $str = 'abbaca'
Output: 'ca'

Step 1: Remove 'bb' => 'aaca'
Step 2: Remove 'aa' => 'ca'
Example 2:
Input: $str = 'azxxzy'
Output: 'ay'

Step 1: Remove 'xx' => 'azzy'
Step 2: Remove 'zz' => 'ay'
Example 3:
Input: $str = 'aaaaaaaa'
Output: ''

Step 1: Remove 'aa' => 'aaaaaa'
Step 2: Remove 'aa' => 'aaaa'
Step 3: Remove 'aa' => 'aa'
Step 4: Remove 'aa' => ''
Example 4:
Input: $str = 'aabccba'
Output: 'a'

Step 1: Remove 'aa' => 'bccba'
Step 2: Remove 'cc' => 'bba'
Step 3: Remove 'bb' => 'a'
Example 5:
Input: $str = 'abcddcba'
Output: ''

Step 1: Remove 'dd' => 'abccba'
Step 2: Remove 'cc' => 'abba'
Step 3: Remove 'bb' => 'aa'
Step 4: Remove 'aa' => ''
File: duplicate-removals
#! /usr/bin/env raku

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

my $index = 0;                                          # [2]

loop                                                    # [3]
{
  last if $index >= $str.chars -1;                      # [4]

  if $str.substr($index,1) eq $str.substr($index +1,1)  # [5]
  {
    say ": Index $index: Remove '{ $str.substr($index,2) }'"
      if $verbose;

    $str.substr-rw($index,2) = "";                      # [5a]
    $index-- unless $index == 0;                        # [5b]
  }
  else                                                  # [6]
  {
    say ": Index $index: Do nothing with '{ $str.substr($index,2) }'"
      if $verbose;

    $index++;                                           # [6a]
  }
}

say $str;                                               # [7]

[1] A string with at least one character. Note the is copy adverb, so that we get a writeable copy. The default is read only.

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

[2] The current index as we iterate.

[3] An eternal loop, with an exit strategy on the very next row; [4].

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

[4] We are finished when we have reached the end (actually just past it) of the modified string.

[5] Compare the character with the current index with the one to the right of it. If equal we remove those two characters, by replacing them (substr-rw) with an empty string [5a] - and moving the current index one position to the left, if possible (i.e. not at zero) [5b].

We do the leftwards index shift as we have to reconsider the previous character after a removal. Consider «ABBA»:

  • Index 0: 0 and 1 differ («A» vs «B»); Do nothing
  • Index 1: 1 and 2 are equal («B» vs «B»); remove them
  • The string is now «AA», and we have to move the index (from 1) to 0 to be able to compare the two «A»s
  • Index 0: 0 and 1 are equal («A» vs «A»); remove them
  • Index 0: The string length is also 0, so we are finished

[6] Not equal? Move the index pointer to the right [6a].

[7] Print the result.

Running it:

$ ./duplicate-removals abbaca
ca

$ ./duplicate-removals azxxzy
ay

$ ./duplicate-removals aaaaaaaa


$ ./duplicate-removals aabccba
a

$ ./duplicate-removals abcddcba

Looking good.

With verbose mode:

$ ./duplicate-removals -v abbaca
: Index 0: Do nothing with 'ab'
: Index 1: Remove 'bb'
: Index 0: Remove 'aa'
: Index 0: Do nothing with 'ca'
ca

$ ./duplicate-removals -v azxxzy
: Index 0: Do nothing with 'az'
: Index 1: Do nothing with 'zx'
: Index 2: Remove 'xx'
: Index 1: Remove 'zz'
: Index 0: Do nothing with 'ay'
ay

$ ./duplicate-removals -v aaaaaaaa
: Index 0: Remove 'aa'
: Index 0: Remove 'aa'
: Index 0: Remove 'aa'
: Index 0: Remove 'aa'


$ ./duplicate-removals -v aabccba
: Index 0: Remove 'aa'
: Index 0: Do nothing with 'bc'
: Index 1: Remove 'cc'
: Index 0: Remove 'bb'
a

$ ./duplicate-removals -v abcddcba
: Index 0: Do nothing with 'ab'
: Index 1: Do nothing with 'bc'
: Index 2: Do nothing with 'cd'
: Index 3: Remove 'dd'
: Index 2: Remove 'cc'
: Index 1: Remove 'bb'
: Index 0: Remove 'aa'

Note that I have chosen to ignore the «lowercase English letters» constraint, allowing anything:

 ./duplicate-removals -v "]FooFööFF]*"
: Index 0: Do nothing with ']F'
: Index 1: Do nothing with 'Fo'
: Index 2: Remove 'oo'
: Index 1: Remove 'FF'
: Index 0: Do nothing with ']ö'
: Index 1: Remove 'öö'
: Index 0: Do nothing with ']F'
: Index 1: Remove 'FF'
: Index 0: Remove ']]'
*

This follows the time-honoured «garbage in, garbage out» tradition.

Challenge #340.2: Ascending Numbers

You are given a string, $str, is a list of tokens separated by a single space. Every token is either a positive number consisting of digits 0-9 with no leading zeros, or a word consisting of lowercase English letters.

Write a script to check if all the numbers in the given string are strictly increasing from left to right.

Example 1:
Input: $str = "The cat has 3 kittens 7 toys 10 beds"
Output: true

Numbers 3, 7, 10 - strictly increasing.
Example 2:
Input: $str = 'Alice bought 5 apples 2 oranges 9 bananas'
Output: false
Example 3:
Input: $str = 'I ran 1 mile 2 days 3 weeks 4 months'
Output: true
Example 4:
Input: $str = 'Bob has 10 cars 10 bikes'
Output: false
Example 5:
Input: $str = 'Zero is 0 one is 1 two is 2'
Output: true

Zero is not a positive number (nor a positive integer, which really is what we are dealing with), but it should clearly be legal tender in our input as it is present in the fifth example. So I choose to allow it.

The first word in each example starts with an uppercase letter, even if the descripton clearly states that only lowercase English letters are allowed. I choose to allow anything non-integer as a word.

File: ascending-numbers-regex
#! /usr/bin/env raku

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

my @words    = $str.split(/\s/);           # [2]
my @integers = @words.grep: * ~~ /^ 0 | (<[1..9]> <[0..9]>*) $/;  # [3]

if $verbose
{
  say ": words: { @words.map({ "'$_'" }).join(", ") }";
  say ": integers: { @integers.map({ "'$_'" }).join(", ") }";
}

say [<] @integers;                        # [4]

[1] A string with at least one character.

[2] We cannot use words to split the string, as it would allow more than one space as separator. That would not be right. But on the other hand, we have deviated from the description quite heavily already...

[3] Only keep the numbers; either a lonely 0 or a non-zero digit followed by zero or more digits.

Want to criminalise zero? Swap the regex with /^<[1..9]> <[0..9]>*$/.

[4] Do the numbers come in increasing order, without duplicates? We use the Reduction Metaoperator [] with < to compute this in one go.

See docs.raku.org/language/operators#Reduction_metaoperators for more information about the Reduction Metaoperator [].

Running it:

$ ./ascending-numbers-regex "The cat has 3 kittens 7 toys 10 beds"
True

$ ./ascending-numbers-regex 'Alice bought 5 apples 2 oranges 9 bananas'
False

$ ./ascending-numbers-regex 'I ran 1 mile 2 days 3 weeks 4 months'
True

$ ./ascending-numbers-regex 'Bob has 10 cars 10 bikes'
False

$ ./ascending-numbers-regex 'Zero is 0 one is 1 two is 2'
True

Looking good.

With verbose mode:

$ ./ascending-numbers-regex -v "The cat has 3 kittens 7 toys 10 beds"
: words: 'The', 'cat', 'has', '3', 'kittens', '7', 'toys', '10', 'beds'
: integers: '3', '7', '10'
True

$ ./ascending-numbers-regex -v 'Alice bought 5 apples 2 oranges 9 bananas'
: words: 'Alice', 'bought', '5', 'apples', '2', 'oranges', '9', 'bananas'
: integers: '5', '2', '9'
False

$ ./ascending-numbers-regex -v 'I ran 1 mile 2 days 3 weeks 4 months'
: words: 'I', 'ran', '1', 'mile', '2', 'days', '3', 'weeks', '4', 'months'
: integers: '1', '2', '3', '4'
True

$ ./ascending-numbers-regex -v 'Bob has 10 cars 10 bikes'
: words: 'Bob', 'has', '10', 'cars', '10', 'bikes'
: integers: '10', '10'
False

$ ./ascending-numbers-regex -v 'Zero is 0 one is 1 two is 2'
: words: 'Zero', 'is', '0', 'one', 'is', '1', 'two', 'is', '2'
: integers: '0', '1', '2'
True

The program actually ignores multiple spaces, regardless of my posturing in [2]:

$ ./ascending-numbers-regex -v 'Zero   is   0   one is 1 two is 2'
: words: 'Zero', '', '', 'is', '', '', '0', '', '', 'one', 'is', '1', 'two', \
  'is', '2'
: integers: '0', '1', '2'
True

It is actually fairly easy to write a program that follows the specification, and not the examples. A grammar comes to mind, but let us do this with a traditional loop.

File: ascending-numbers-pedantic
#! /usr/bin/env raku

unit sub MAIN ($str where $str.chars > 0,
               :u(:$ucfirst),
	       :z(:$allow-zero),
               :v(:$verbose));

my @words    = $str.split(/\s/);
my $error    = 0;
my @integers;

for @words -> $word
{
  print ": Token '$word'" if $verbose;

  if $word ~~ /^ <[1..9]> <[0..9]>* $/
  {
    @integers.push: $word;
    say " is a positive integer" if $verbose;
  }
  elsif $allow-zero && $word eq "0"
  {
    @integers.push: $word;
    say " is a positive integer" if $verbose;
  }
  elsif $word ~~ /^ <[a..z]>+ $/
  {
    say " is an English lowercase word" if $verbose
  }
  elsif $ucfirst && $word ~~ /^ <[A..Z]><[a..z]>* $/
  {
    say " is an English lowercase word" if $verbose
  }
  else
  {
    say " is illegal";
    $error++;
  }
}

say $error ?? False !! [<] @integers;

This program will reject all the examples. Allow zeroes with the «-z» command line option, and an optional initial uppercase letter (in all the words, not just the very first one) with «-z».

Feel free to try it out.

Note that words with hyphens, even though they are time-honoured, will fail. As will names with embedded uppercase letters, as e.g. «McDonald», or an apostrophe (as e.g. «O'Brian»).

And that's it.