String the Atoms
with Raku

by Arne Sommer

String the Atoms with Raku

[412] Published 23. August 2026.

This is my response to The Weekly Challenge #387.

#387.1 Rearrange Binary String You are given a binary string string.

Write a script to re-arrange the given binary string that all occurrences of “01” are simultaneously replaced with “10” until no occurrences of “01” exist. Finally return the total steps needed.

Example 1:
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

#387.2 Atoms Count You are given a chemical formula with elements, numbers, and parentheses.

Write a script to count the total number of each type of atom by expanding all grouped multipliers. Then, format and return the final inventory as a single string sorted alphabetically by element name, including the total count only if it is greater than 1.

Example 1:
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)
File: atoms-count
#! /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.