Roman Numeral Converter
Roman numerals are an ancient numbering system that uses letters instead of digits.
Some common values are:
I = 1 V = 5 X = 10 L = 50 C = 100 D = 500 M = 1000
I = 1 V = 5 X = 10 L = 50 C = 100 D = 500 M = 1000
The goal of this tutorial is to convert a normal integer like:
1987
1987
into:
MCMLXXXVII
MCMLXXXVII
The Idea
The easiest way to convert an integer into Roman numerals is to use a rule table.
The table starts with the largest Roman numeral values and works down to the smallest.
1000 -> M 900 -> CM 500 -> D 400 -> CD 100 -> C 90 -> XC 50 -> L 40 -> XL 10 -> X 9 -> IX 5 -> V 4 -> IV 1 -> I
1000 -> M 900 -> CM 500 -> D 400 -> CD 100 -> C 90 -> XC 50 -> L 40 -> XL 10 -> X 9 -> IX 5 -> V 4 -> IV 1 -> I
For each rule, we check whether the current number is at least that large. If it is, we add the Roman symbol to the output and subtract the rule value.
For example, 1987 starts with 1000, so we add M and subtract 1000, leaving 987. Then 900 fits, so we add CM and subtract 900, leaving 87.
We keep going until the number reaches zero.
Version 1: First Working Version
The first version is a direct translation of the algorithm.
We store Roman numeral rules in a table and then walk through them from largest to smallest.
roman_to_int_map | { "I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000 } int_to_roman_rules | [ { value: 1000, symbol: "M" }, { value: 900, symbol: "CM" }, { value: 500, symbol: "D" }, { value: 400, symbol: "CD" }, { value: 100, symbol: "C" }, { value: 90, symbol: "XC" }, { value: 50, symbol: "L" }, { value: 40, symbol: "XL" }, { value: 10, symbol: "X" }, { value: 9, symbol: "IX" }, { value: 5, symbol: "V" }, { value: 4, symbol: "IV" }, { value: 1, symbol: "I" } ] act int_to_roman(num) result | "" repeat int_to_roman_rules as rule value | rule{"value"} symbol | rule{"symbol"} repeat num >= value result |= result + symbol num |= num - value xx xx return result xx :say(int_to_roman(1)) :say(int_to_roman(4)) :say(int_to_roman(9)) :say(int_to_roman(42)) :say(int_to_roman(1987)) :say(int_to_roman(1994))
roman_to_int_map | { "I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000 } int_to_roman_rules | [ { value: 1000, symbol: "M" }, { value: 900, symbol: "CM" }, { value: 500, symbol: "D" }, { value: 400, symbol: "CD" }, { value: 100, symbol: "C" }, { value: 90, symbol: "XC" }, { value: 50, symbol: "L" }, { value: 40, symbol: "XL" }, { value: 10, symbol: "X" }, { value: 9, symbol: "IX" }, { value: 5, symbol: "V" }, { value: 4, symbol: "IV" }, { value: 1, symbol: "I" } ] act int_to_roman(num) result | "" repeat int_to_roman_rules as rule value | rule{"value"} symbol | rule{"symbol"} repeat num >= value result |= result + symbol num |= num - value xx xx return result xx :say(int_to_roman(1)) :say(int_to_roman(4)) :say(int_to_roman(9)) :say(int_to_roman(42)) :say(int_to_roman(1987)) :say(int_to_roman(1994))
This version works. It loops through every rule, pulls out the value and symbol, and keeps applying that rule while the current number is large enough.
The code is still a little noisy because every loop iteration starts with:
value | rule{"value"} symbol | rule{"symbol"}
value | rule{"value"} symbol | rule{"symbol"}
The algorithm itself only cares about those two names: value and symbol.
So we can tighten the loop.
Final Version
The final version binds value and symbol directly in the repeat statement.
table | [ { value: 1000, symbol: "M" }, { value: 900, symbol: "CM" }, { value: 500, symbol: "D" }, { value: 400, symbol: "CD" }, { value: 100, symbol: "C" }, { value: 90, symbol: "XC" }, { value: 50, symbol: "L" }, { value: 40, symbol: "XL" }, { value: 10, symbol: "X" }, { value: 9, symbol: "IX" }, { value: 5, symbol: "V" }, { value: 4, symbol: "IV" }, { value: 1, symbol: "I" } ] act int_to_roman(n) out | .. repeat table as value, symbol repeat n >= value out |= out + symbol n |= n - value xx xx return out xx :say(int_to_roman(1)) /// EXPECT I :say(int_to_roman(4)) /// EXPECT IV :say(int_to_roman(9)) /// EXPECT IX :say(int_to_roman(42)) /// EXPECT XLII :say(int_to_roman(1987)) /// EXPECT MCMLXXXVII :say(int_to_roman(1994)) /// EXPECT MCMXCIV
table | [ { value: 1000, symbol: "M" }, { value: 900, symbol: "CM" }, { value: 500, symbol: "D" }, { value: 400, symbol: "CD" }, { value: 100, symbol: "C" }, { value: 90, symbol: "XC" }, { value: 50, symbol: "L" }, { value: 40, symbol: "XL" }, { value: 10, symbol: "X" }, { value: 9, symbol: "IX" }, { value: 5, symbol: "V" }, { value: 4, symbol: "IV" }, { value: 1, symbol: "I" } ] act int_to_roman(n) out | .. repeat table as value, symbol repeat n >= value out |= out + symbol n |= n - value xx xx return out xx :say(int_to_roman(1)) /// EXPECT I :say(int_to_roman(4)) /// EXPECT IV :say(int_to_roman(9)) /// EXPECT IX :say(int_to_roman(42)) /// EXPECT XLII :say(int_to_roman(1987)) /// EXPECT MCMLXXXVII :say(int_to_roman(1994)) /// EXPECT MCMXCIV
Output:
I IV IX XLII MCMLXXXVII MCMXCIV
I IV IX XLII MCMLXXXVII MCMXCIV
What We Learned
This converter uses two different kinds of repetition.
The outer loop repeats over the rule table.
repeat table as value, symbol
repeat table as value, symbol
The inner loop repeats while the current number is still large enough to use the current rule.
repeat n >= value
repeat n >= value
Together, those two loops express the whole algorithm.
Other languages usually write this as a for loop wrapped around a while loop.
Goblin uses repeat for both.
The input decides what kind of repetition happens.