Peter’s blog ✴ Week 387 ✴ 17 August 2026

THE WEEKLY CHALLENGE
Ones and atoms

The Perl Camel

Task 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.

Examples


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)

Analysis

This task can be achieved most easily by analysing the supplied string from right to left. That can be awkward, so I start by reversing the string, and then split it into 'tokens' from left to right.

A token can be any of 5 types:

  • one or more digits ($n) followed by a closing bracket ')'
  • just one or more digits ($m)
  • zero or one lower case letters followed by an upper case one eg H or eH ($x)
  • an opening bracket '('
  • a closing bracket ')'

When I find a number-bracket token I multiply $n by the number, and when I meet the corresponding open bracket I divide $n by that number, which I've saved on a stack (@s).

So, when I encounter an element symbol, I have already parsed the number following it ($m), and any numbers following the brackets which enclose the symbol.

I can then increment $c{$x} by $n * $m so that when I'm finished, for example, $c{'He'} == 3.

And then sort keys %c gives the answer

This works fine with a valid input string, but in retrospect I should have raised an error in the final else to avoid an infinite loop on certain invalid inputs.

And lastly, as a chemist, I might point out that there are rules and conventions regarding the ordering of chemical formulae, and the output of this algorithm is very non-compliant!

Perl Weekly’s review

from PW issue 787

The post presents a brief but informative study of Perl Weekly Challenge 387, emphasising the use of brevity in idiomatic Perl. It discusses an quality one-liner, which implements a global regex substitution in a while loop to accomplish the job of counting steps with the least amount of overhead possible.

This review may cover either or both challenges for this week.

Try it 

Your input:



eg: Mg3(PO4)2

Script


#!/usr/bin/perl

# Blog: http://ccgi.campbellsmiths.force9.co.uk/challenge/387/2

use v5.26;    # The Weekly Challenge - 2026-08-17
use utf8;     # Week 387 - task 2 - Atoms count
use warnings; # Peter Campbell Smith
binmode STDOUT, ':utf8';
use Encode;

atoms_count('((N2O)3(H2O)2)2');
atoms_count('Mg3(PO4)2');
atoms_count('(((H)2)3)4');
atoms_count('NaCl3(O2(S10)2)2Mg');
atoms_count('Z2Y3(X2W)2');

sub atoms_count {
    
    my ($f, $m, $n, %c, @s, $k);
    
    # initialise
    $f = shift;
    say qq[\nInput:  '$f'];
    $m = $n = 1;
    %c = ();

    # tokenise
    $f = reverse($f);
    while ($f) {
        
        # digits + close bracket
        if ($f =~ m|^(\d+)\)(.*)|) {
            push @s, $1;
            $m *= $1;           
        
        # digits
        } elsif ($f =~ m|^(\d+)(.*)|) {
            $n = reverse($1);

        # symbol A or Ab            
        } elsif ($f =~ m|^([a-z]?[A-Z])(.*)|) {
            $c{reverse($1)} += $n * $m;
            $n = 1;
        
        # open bracket
        } elsif ($f =~ m|^(\()(.*)|) {
            $m /= pop @s;
            
        # close bracket
        } elsif ($f =~ m|^(\))(.*)|) {
            
        } else { 
            last;
        }
        $f = $2;
    }
    
    # report
    print qq[Output: '];
    for $k (sort keys %c) {
        $c{$k} = '' if $c{$k} < 2;
        print qq[$k$c{$k}];
    }
    say q['];
}
 

27 lines of code

Output from script


Input:  '((N2O)3(H2O)2)2'
Output: 'H8N12O10'

Input:  'Mg3(PO4)2'
Output: 'Mg3O8P2'

Input:  '(((H)2)3)4'
Output: 'H24'

Input:  'NaCl3(O2(S10)2)2Mg'
Output: 'Cl3MgNaO4S40'

Input:  'Z2Y3(X2W)2'
Output: 'W2X4Y3Z2'

 

Any content of this website which has been created by Peter Campbell Smith is in the public domain