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( ($<thousands>.made // 0) + ($<hundreds>.made // 0) + ($<tens>.made // 0) + ($<ones>.made // 0) ) } } 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 $n1 = $<roman-number>[0].made; my $n2 = $<roman-number>[1].made; 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 $answer = RomanArithmetics.parse($input).made; my $output = "$input => ($answer) "; if $answer != $answer.round { $output ~= "non potest (they didn't do fractions)"; } elsif $answer >= 4000 { $output ~= "non potest (they only went up to 3999)"; } elsif $answer == 0 { $output ~= "nulla (they knew about zero but didn't have a symbol)"; } elsif $answer < 0 { $output ~= "non potest (they didn't do negative numbers)"; } else { $output ~= to-roman($answer); } 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)