This is my response to The Weekly Challenge #387.
Input: $str = "111000"
Output: 0
The string already has all 1s on the left and 0s on the right.
There are no occurrences of "01", so zero step needed.
Example 2:
Input: $str = "00011"
Output: 4
Step 1: "00101"
Step 2: "01010"
Step 3: "10100"
Step 4: "11000"
Example 3:
Input: $str = "01011"
Output: 3
Step 1: "10101"
Step 2: "11010"
Step 3: "11100"
Example 4:
Input: $str = "010101"
Output: 3
Step 1: "101010"
Step 2: "110100"
Step 3: "111000"
Example 5:
Input: $str = "00001"
Output: 4
Step 1: "00010"
Step 2: "00100"
Step 3: "01000"
Step 4: "10000"
My first take on this, without reading the text carefully, involves a single loop. Easy. But wrong...
File: rebist-wrong#! /usr/bin/env raku
subset Binary where /^ <[01]>+ $/;
unit sub MAIN (Binary $str is copy, :v(:$verbose));
my $count = 0;
while $str!~~ /01/
{
my $index = $str.index("01");
my $old = $str;
$str.substr-rw($index,2) = "10";
$count++;
say "[$count] Index: $index,{ $index +1} Old: $old new: $str"
if $verbose;
}
say $count;
[3+4] A custom type, set up with subset, to ensure
a binary number.
See docs.raku.org/language/typesystem#subset for more information about subset.
[7] The result will end up here.
[9] As long as we have the string 01 in our input.
[11] Get the index of that substring; the first one from the left if there are more than one.
[13] Replace the substring with substr-rw.
See docs.raku.org/routine/substr-rw for more information about substr-rw.
[14] Count the replacement.
[19] Print the final count.
Running it on the second example, where we should get 4:
$ ./rebist-wrong -v "00011"
:[1] Index: 2,3 Old: 00011 new: 00101
:[2] Index: 1,2 Old: 00101 new: 01001
:[3] Index: 0,1 Old: 01001 new: 10001
:[4] Index: 3,4 Old: 10001 new: 10010
:[5] Index: 2,3 Old: 10010 new: 10100
:[6] Index: 1,2 Old: 10100 new: 11000
6
The problem is the word «simultaneously». We should replace all occurences at the same time, as a single step. Then continue until we have reached the goal. We'll need a second loop for that...
File: rebist#! /usr/bin/env raku
subset Binary where /^ <[01]>+ $/;
unit sub MAIN (Binary $str is copy, :v(:$verbose));
my $count = 0;
my $round = 0;
while $str ~~ /01/
{
my $index = $str.index("01");
my @indices;
$round++;
while defined $index
{
@indices.push: $index;
$index = $str.index("01", $index + 1);
}
for @indices -> $index
{
my $old = $str;
$str.substr-rw($index, 2) = "10";
$count++;
say "Round: $round [$count] Index: $index,{ $index +1} \
Old: $old new: $str" if $verbose;
}
}
say $round;
[10] The outermost loop, as long as we have unfinished business.
[12] Get the index of the (first) swap.
[17] As long as we get a new index,
[19] Push it to a list.
[20] Look for the next index (starting after the current one).
[23-31] Here we iterate over the indices (one or more) in this step. The inner code here should be familiar.
Running it:
$ ./rebist "111000"
0
$ ./rebist "00011"
4
$ ./rebist "01011"
3
$ ./rebist "010101"
3
$ ./rebist "00001"
4
Looking good.
With verbose mode:
$ ./rebist -v "111000"
0
$ ./rebist -v "00011"
:Round: 1 [1] Index: 2,3 Old: 00011 new: 00101
:Round: 2 [2] Index: 1,2 Old: 00101 new: 01001
:Round: 2 [3] Index: 3,4 Old: 01001 new: 01010
:Round: 3 [4] Index: 0,1 Old: 01010 new: 10010
:Round: 3 [5] Index: 2,3 Old: 10010 new: 10100
:Round: 4 [6] Index: 1,2 Old: 10100 new: 11000
4
$ ./rebist -v "01011"
:Round: 1 [1] Index: 0,1 Old: 01011 new: 10011
:Round: 1 [2] Index: 2,3 Old: 10011 new: 10101
:Round: 2 [3] Index: 1,2 Old: 10101 new: 11001
:Round: 2 [4] Index: 3,4 Old: 11001 new: 11010
:Round: 3 [5] Index: 2,3 Old: 11010 new: 11100
3
$ ./rebist -v "010101"
:Round: 1 [1] Index: 0,1 Old: 010101 new: 100101
:Round: 1 [2] Index: 2,3 Old: 100101 new: 101001
:Round: 1 [3] Index: 4,5 Old: 101001 new: 101010
:Round: 2 [4] Index: 1,2 Old: 101010 new: 110010
:Round: 2 [5] Index: 3,4 Old: 110010 new: 110100
:Round: 3 [6] Index: 2,3 Old: 110100 new: 111000
3
$ ./rebist -v "00001"
:Round: 1 [1] Index: 3,4 Old: 00001 new: 00010
:Round: 2 [2] Index: 2,3 Old: 00010 new: 00100
:Round: 3 [3] Index: 1,2 Old: 00100 new: 01000
:Round: 4 [4] Index: 0,1 Old: 01000 new: 10000
4
Input: $formula = "((N2O)3(H2O)2)2"
Output: "H8N12O10"
Step 1: Expand the innermost parentheses
(N2O)3 => N = 2*3 = 6, O = 1*3 = 3 => N6O3
(H2O)2 => H = 2*2 = 4, O = 1*2 = 2 => H4O2
Step 2: Combine inside the outer parentheses
Formula becomes: (N6O3 H4O2)2
Sum up identical elements inside: (N6 H4 O5)2
Step 3: Apply the outer multiplier
N = 6*2 = 12
H = 4*2 = 8
O = 5*2 = 10
Step 4: Sort alphabetically and format
Alphabetical order: H, N, O
Counts: H: 8, N: 12, O: 10
Example 2:
Input: $formula = "Mg3(PO4)2"
Output: "Mg3O8P2"
Step 1: Parse ungrouped elements
Mg3 => Mg = 3
Step 2: Expand parentheses (PO4)2
P = 1*2 = 2
O = 4*2 = 8
Step 3: Total up counts
Mg = 3
P = 2
O = 8
Step 4: Sort alphabetically and format
Alphabetical order: Mg, O, P
Counts: Mg: 3, O: 8, P: 2
Example 3:
Input: $formula = "(((H)2)3)4"
Output: "H24"
Step 1: Expand innermost level (H)2
H = 1*2 = 2 => formula becomes ((H2)3)4
Step 2: Expand middle level (H2)3
H = 2*3 = 6 => formula becomes (H6)4
Step 3: Expand outer level (H6)4
H = 6*4 = 24
Step 4: Sort alphabetically and format
Single element: H: 24
Example 4:
Input: $formula = "NaCl3(O2(S10)2)2Mg"
Output: "Cl3MgNaO4S40"
Step 1: Expand innermost parentheses (S10)2
S = 10*2 = 20 => inner formula becomes => O2S20
Step 2: Expand outer parentheses (O2S20)2
O = 2*2 = 4
S = 20*2 = 40
Step 3: Combine all parts
Ungrouped start: Na (Na = 1), Cl3 (Cl = 3)
Expanded middle: O = 4, S = 40
Ungrouped end: Mg (Mg = 1)
Step 4: Sort alphabetically and format
Alphabetical order: Cl (3), Mg (1), Na (1), O (4), S (40)
Omit the number 1 for Mg and Na.
Example 5:
Input: $formula = "Z2Y3(X2W)2"
Output: "W2X4Y3Z2"
Step 1: Parse ungrouped elements
Z2 => Z = 2
Y3 => Y = 3
Step 2: Expand parentheses (X2W)2
X = 2*2 = 4
W = 1*2 = 2
Step 3: Total up counts
W = 2, X = 4, Y = 3, Z = 2
Step 4: Sort alphabetically and format
Alphabetical order: W (2), X (4), Y (3), Z (2)
#! /usr/bin/env raku
unit sub MAIN (Str $formula, :v(:$verbose));
my @stack = ({},);
my $i = 0;
while $i < $formula.chars
{
my $ch = $formula.substr($i, 1);
if $ch eq '('
{
@stack.push({});
$i++
}
elsif $ch eq ')'
{
my %group = @stack.pop;
$i++;
my $mult = 0;
while $i < $formula.chars && $formula.substr($i, 1) ~~ /\d/
{
$mult = $mult * 10 + $formula.substr($i, 1).Int;
$i++
}
$mult = 1 unless $mult;
for %group.kv -> $k, $v
{
@stack[*-1]{$k} += $v * $mult;
}
}
else
{
my $elem = $ch;
$i++;
while $i < $formula.chars && $formula.substr($i, 1)
~~ /<[a..z]>/
{
$elem ~= $formula.substr($i, 1);
$i++
}
my $count = 0;
while $i < $formula.chars && $formula.substr($i, 1) ~~ /\d/
{
$count = $count * 10 + $formula.substr($i, 1).Int;
$i++
}
$count = 1 unless $count;
@stack[*-1]{$elem} += $count;
}
}
my %final = @stack[0];
say %final.keys.sort.map({ ~$_ ~ (%final{$_} > 1 ?? %final{$_}
!! '') }).join;
[3] The formula, without any checks.
[5] Each sub part (something inside a set of parens), will be placed here while we do the parsing. When finished, we will have one element here, the result, in this very first hash.
[6] The index of the current character in the forumla. (I could have
iterated over the characters themselves, after a comb, but that
would require a major rewrite.)
A better approach could have been using
gather/take to group the characters.
[8] Until we reach the end.
[10] Get the next character.
[12] An opening parentesis?
[14] Add a new hash to the stack (for the content of this new group we are about to consume).
[15] Ready for the next character.
[17] An ending parentesis?
[19] Get the group (from the stack).
[20] Ready for the next character.
[21-27] Get the integer, digit by digit.
[29] A missing integer means that we have just 1 instance.
[31] Iterate over the elements in the current group.
[33] Add the modified number of the element to the previous hash.
[36] Here we should have an uppercase letter.
[38-46] Extract the uppercase letter, and add on any following lowercase ones.
[48-55] The same as 21-29, but applied to a single element and not a group.
[60] The result is in the first hash.
[62] Pretty print the result, in alphabetical order. A count of one is
shown as an empty string (instead of a 1).
Running it:
$ ./atoms-count "((N2O)3(H2O)2)2"
H8N12O10
$ ./atoms-count "Mg3(PO4)2"
Mg3O8P2
$ ./atoms-count "(((H)2)3)4"
H24
$ ./atoms-count "NaCl3(O2(S10)2)2Mg"
Cl3MgNaO4S40
$ ./atoms-count "Z2Y3(X2W)2"
W2X4Y3Z2
Looking good.
I did not have time to make verbose mode work in a sensible way.
And that's it.