# Calculator with Roman numbers using Raku Grammars

Using Raku grammar, I created a simple calculator that works with Roman numbers, for example: `XXI + MCMXIX`.

The second task of Weekly Challenge 227 is an interesting problem to create a simple calculator, which will work with Roman numbers.

Write a script to handle a 2-term arithmetic operation expressed in Roman numeral.

Example

IV + V     => IX
M - I      => CMXCIX
X / II     => V
XI * VI    => LXVI
VII ** III => CCCXLIII
V - V      => nulla (they knew about zero but didn't have a symbol)
V / II     => non potest (they didn't do fractions)
MMM + M    => non potest (they only went up to 3999)
V - X      => non potest (they didn't do negative numbers)

My first reaction is to use Raku’s grammars. And I have prepared the fundamentals for solving this kind of tasks already, namely:

Please refer to the materials above for the details, but in brief, the idea of converting any given Roman number to its decimal value is to use a grammar that parses it and adds up to the result based on what it sees.

A Roman number is a sequence of patterns that represent thousands, hundreds, tens, and ones. So, here is the modified grammar from one of the above posts:

grammar RomanArithmetics {

. . .

token roman-number {
<thousands>? <hundreds>? <tens>? <ones>? {
\$/.make(
)
}
}

token thousands {
| M    { \$/.make(1000) }   | MM   { \$/.make(2000) }
| MMM  { \$/.make(3000) }   | MMMM { \$/.make(4000) }
}

token hundreds {
| C    { \$/.make(100) }    | CC   { \$/.make(200) }
| CCC  { \$/.make(300) }    | CD   { \$/.make(400) }
| D    { \$/.make(500) }    | DC   { \$/.make(600) }
| DCC  { \$/.make(700) }    | DCCC { \$/.make(800) }
| CM   { \$/.make(900) }
}

token tens {
| X    { \$/.make(10) }     | XX   { \$/.make(20) }
| XXX  { \$/.make(30) }     | XL   { \$/.make(40) }
| L    { \$/.make(50) }     | LX   { \$/.make(60) }
| LXX  { \$/.make(70) }     | LXXX { \$/.make(80) }
| XC   { \$/.make(90) }
}

token ones {
| I    { \$/.make(1) }      | II   { \$/.make(2) }
| III  { \$/.make(3) }      | IV   { \$/.make(4) }
| V    { \$/.make(5) }      | VI   { \$/.make(6) }
| VII  { \$/.make(7) }      | VIII { \$/.make(8) }
| IX   { \$/.make(9) }
}
}

In terms of grammar, a Roman number is <thousands>? <hundreds>? <tens>? <ones>, where each part is optional. To collect the decimal value, I am using the AST to pass an integer value to the next level.

For example, for the number XXI our grammar will find two tokens: XX and I, which are converted to 20 and 1. At the top level, these partial values are summed up together to get 21.

As we need a basic calculator, let’s add the corresponding rules directly to the RomanArithmetics grammar:

grammar RomanArithmetics {
rule TOP {
<roman-number> <op> <roman-number> {

my \$n;
given ~\$<op> {
when '+'  {\$n = \$n1 +  \$n2}
when '-'  {\$n = \$n1 -  \$n2}
when '*'  {\$n = \$n1 *  \$n2}
when '/'  {\$n = \$n1 /  \$n2}
when '**' {\$n = \$n1 ** \$n2}
}

\$/.make(\$n)
}
}

token op {
'+' | '-' | '*' | '/' | '**'
}

. . .
}

Here, the TOP rule expects a string consisting of two Roman numbers with an operation symbol op between them. Value computation happens immediately in the inline actions such as \$n = \$n1 + \$n2.

The main part of the program is done. What remains is the opposite conversion to print the result and a straightforward set of tests to print an error message if the result cannot be represented with a Roman number.

First, the reverse convertion:

sub to-roman(\$n is copy) {
state @roman =
1000 => < M MM MMM >,
100 => < C CC CCC CD D DC DCC DCCC CM >,
10  => < X XX XXX XL L LX LXX LXXX XC >,
1   => < I II III IV V VI VII VIII IX >;

my \$roman;

for @roman -> \$x {
my \$digit = (\$n / \$x.key).Int;
\$roman ~= \$x.value[\$digit - 1] if \$digit;
\$n %= \$x.key;
}

return \$roman;
}

And finally, the function that refer to the grammar and prints the result.

sub compute(\$input) {

my \$output = "\$input => (\$answer) ";

\$output ~= "non potest (they didn't do fractions)";
}
\$output ~= "non potest (they only went up to 3999)";
}
\$output ~= "nulla (they knew about zero but didn't have a symbol)";
}
\$output ~= "non potest (they didn't do negative numbers)";
}
else {
}

return \$output;
}

To test the program, let us equip it with the test cases from the problem description and call them one by one:

my @test-cases =
'IV + V',
'M - I',
'X / II',
'XI * VI',
'VII ** III',
'V - V',
'V / II',
'MMM + M',
'V - X'
;

say compute(\$_) for @test-cases;

The program prints the following. I also added decimal value to the output so that we can see why each of the error messages was chosen.

\$ raku ch-2.raku
IV + V => (9) IX
M - I => (999) CMXCIX
X / II => (5) V
XI * VI => (66) LXVI
VII ** III => (343) CCCXLIII
V - V => (0) nulla (they knew about zero but didn't have a symbol)
V / II => (2.5) non potest (they didn't do fractions)
MMM + M => (4000) non potest (they only went up to 3999)
V - X => (-5) non potest (they didn't do negative numbers)