Fun with Perl 6 using Roman Numerals

Daniel Mita
Jun 1 · 4 min read

I recently took up solving some of the Perl weekly challenges, the most recent being the Roman Numeral encoder/decoder. In this post I’m going to go through how I put my solution together and how it works.


Defining The Numerals

To start off with, I declared a few constants. The first two being the fundamental set of characters for our numerals:

constant @letters   = 「IVXLCDM」.comb;
constant @overlines = "\c[combining overline]", "\c[combining double overline]"; # These represent x 1000 and x 1000000
constant @letter-pairs = reverse # … the following code

These constants are then used to give us a list of all of our numerals:

@letters[0], |( @letters, |@overlines.map( @letters X~ * ) ).map({
  gather {
    for .rotor(3 => -1) -> @group {
      for 1, 2 {
        take @group[0, $_].join;
        take @group[$_];
      }
    }
  }
}).flat;

We start of by taking our I from the list. Following that, we create a list of three groups of letters:

@letters, |@overlines.map( @letters X~ * )
# (I V X L C D M)(I̅ V̅ X̅ L̅ C̅ D̅ M̅)(I̿ V̿ X̿ L̿ C̿ D̿ M̿)

We split each of these into overlapping groups of three using rotor, and then use the elements from each of those lists to take our numerals:

.rotor(3 => -1)
# ((I V X) (X L C) (C D M))
# .[0,1], .[1], .[0,2], .[2] on each group gives us IV, V, IX, X etc

Now we want to assign all of these numerals their appropriate values. We do that using the following sequence:

1, |( * X* 4, 5, 9, 10 ) … ∞

What this sequence does is take the 1 and gives us that multiplied by 4, 5, 9 and 10. With the 10 now on the end of the list, the sequence is repeated, giving us 40, 50, 90 and 100. This continues ad infinitum for as many numbers as we need.

Taking our list of numerals and zipping ( z=> ) them with the numbers gives us the list of pairs we’ll be using:

:I(1), :IV(4), :V(5), :IX(9), :X(10), :XL(40) …

Converting Numbers to Roman Numerals

given %(@letter-pairs.Map.antipairs) -> %letter-map {
  return [~] gather {
    for $number.flip.comb.pairs.reverse {
      given 10 ** .key -> $key {
        when .value == 4 | 9 {
          take %letter-map{ $key * .value };
        }
        take %letter-map{ $key * 5 } if .value ≥ 5;
        take %letter-map{ $key } x .value % 5;
      }
    }
  };
}

We start off with taking the list of pairs we defined earlier, and turning it into a hash with the keys being the Arabic number each Roman numeral represents. We then take the input (being $number here) and transform it with:

12345.flip.comb.pairs.reverse
# 4 => 1, 3 => 2, 2 => 3, 1 => 4, 0 => 5

This gives us a place value for each digit we’re going to use, 10 ** .key giving us 10000, 1000, 100, etc.

We’re then going to take each Roman numeral based on what we got:

when .value == 4 | 9 {
  take %letter-map{ $key * .value };
}
take %letter-map{ $key * 5 } if .value ≥ 5;
take %letter-map{ $key } x .value % 5;
}

With the when block, if the condition matches, control is returned to the parent block and any subsequent code is skipped. This would happen with our 1 => 4 In $key we would have 10. We look in the hash for the character assigned to 10 × 4, which is 40 => "XL", and take that to be used in our eventual string. If the value is not 4 or 9, then we instead take the character representing our 5s, if we need it, and then a string of 1s, between 0 or 3 depending on what our value is.

12345 gives us X̅MMCCCXLV.


Converting Roman Numerals to Numbers

return [+] gather {
  my $str = $roman-string.uc;
  for @letter-pairs -> $pair {
    if $str ~~ / ^ ( $($pair.key) )+ / {
      take ($pair.value xx $0).Slip;
      $str.=substr($0.join.chars);
    }
  }
}

Our letter pairs are sorted from highest to lowest, as that’s typically how they’re given in Roman numeral format. Starting with our largest numeral, we take our string and attempt to match the start of it with a regex. The match is then stored in $0.

If we did get a match, we take the value of that numeral, multiplied by how many we found e.g. if the numeral has XXX we take 10 three times. We then take the number of characters in that match and cut them off our string for our next iteration.

While there is some error checking (we throw an error if the string isn’t empty by the end), the routine is not very strict, and will still give a result if you enter a series of characters like XXXXX. It will however take the resulting value, convert it back to Roman to compare, and ask if you meant L 😉

Daniel Mita

Written by

I am a professional Perl programmer, and in my spare time I do mentoring and maintenance for the Perl 5 and 6 tracks on https://exercism.io/