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)