ddonche/goblin-lang
0.46.24
1
0
docs tutorial
[[roman-numeral-converter]]

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.