Let’s start with a simple mapping between some key Roman numerals and their Arabic counterparts.
const roman2arabic = [("M", 1000), ("CM", 900),
("D", 500), ("CD", 400),
("C", 100), ("XC", 90),
("L", 50), ("XL", 40),
("X", 10), ("IX", 9), ("V", 5),
("IV", 4), ("I", 1)]
The mapping is defined with (const
) keyword to signal that we do not wish to change it throughout the program execution. Moreover, we used a vector of tuples, not a dictionary, since we want to preserve the (descending) order of the pairs of values. Notice, that we also incluced the key landmarks of subtractive notation (e.g. ("CM", 900)
or ("IV", 4)
). Now, we are ready to take the next step.
function getRoman(arabic::Int)::Str
@assert 0 < arabic < 4000
roman::Str = ""
for (r, a) in roman2arabic
while(arabic >= a)
roman *= r
arabic -= a
end
end
return roman
end
We will build our Roman numeral bit by bit starting from an empty string (roman::Str = ""
). For that we traverse all our Roman and Arabic landmarks (for (r, a) in roman2arabic
). For each of them (starting from the highest number), we check if the currently examined Arabic landmark (a
) is lower than the Arabic number we got to translate (arabic
). As long as it is (while(arabic >= a)
) we append the parallel Roman landmark (r
) to our solution (roman *= r
) and subtract the Arabic landmark (a
) from our Arabic number (arabic -= a
). Once we are done we return the result.
Note. To better understand the above code you may read about the
while
loop in the docs and use show macro to have a sneak peak how the variables in the loop change.
Time for a minitest (go ahead pick a number and, in your head or on a piece of paper, follow the function’s execution).
getRoman.(1:10)
["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"]
OK, time for a bigger test.
getRoman.(arabicTest) == romanTest
true
Looks alright.
Time to write our getArabic
function. For that we will have to break a Roman numeral into tokens (from left to right) that we will use to build up an Arabic number.
const romanTokens = map(first, roman2arabic)
# equivalent to
const romanTokens = map(tuple -> tuple[1], roman2arabic)
function getTokenAndRest(roman::Str)::Tuple{Str, Str}
if length(roman) <= 1
return (roman, "")
elseif roman[1:2] in romanTokens
return (roman[1:2], string(roman[3:end]))
else
return (string(roman[1]), string(roman[2:end]))
end
end
function getTokens(roman::Str)::Vec{Str}
curToken::Str = ""
allTokens::Vec{Str} = []
while (roman != "")
curToken, roman = getTokenAndRest(roman)
push!(allTokens, curToken)
end
return allTokens
end
First, we extract romanTokens
with map
and first. Next, we use getTokenAndRest
to split a Roman numeral into a key token (first on the left, either one or two characters long) and the rest of the numeral. The string(sth)
part makes sure we always return a string and not a character. Anyway, based on it (getTokenAndRest
) we split a Roman numeral into a vector of tokens with getTokens
. Now, we are ready to write our getArabic
.
function getVal(lookup::Vec{Tuple{Str, Int}}, key::Str, default::Int)::Int
for (k, v) in lookup
if k == key
return v
end
end
return default
end
function getArabic(roman::Str)::Int
tokens::Vec{Str} = getTokens(roman)
sum::Int = 0
for curToken in tokens
sum += getVal(roman2arabic, curToken, 0)
end
return sum
end
Notice, that before we moved to getArabic
we wrote getVal
that will provide us with an Arabic number for a given Roman numeral token. The above was not strictly necessary, as we could have just use the built-in get for that purpose (e.g. with get(Dict(roman2arabic), key, default)
). Anyway, to get the Arabic number for a given Roman numeral (roman
), first we split it into a vector of tokens
and traverse them one by one (curToken
) with the for
loop. We translate curToken
to an Arabic numeral and add it to sum
which we return once we are done.
Again, let’s go with a minitest (go ahead pick a number and, in your head or on a piece of paper, follow the function’s execution).
getArabic.(["I", "II", "III", "IV", "V", "VI", "VII", "VIII"])
[1, 2, 3, 4, 5, 6, 7, 8]
Finally, a bigger test.
getArabic.(romanTest) == arabicTest
true
And another one.
getArabic.(getRoman.(arabicTest)) == arabicTest
true
I don’t know about you, but to me the results are satisfactory.