This is my response to The Weekly Challenge #340.
$str
, consisting of lowercase English
letters.
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' => ''
#! /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»:
[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.
$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.
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.