Roman numerals are about the most useless things on the planet nowadays, provided you aren’t rolling credits at a movie screen.
However, if the Romans DO return, we have a solution to the pesky number problem. I present you my F# implementation.
|let vals = [(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")]|
|let toRoman iVal =|
|let rec loop acc n list =|
|match list with|
||  -> acc|
|| (i, s)::xs when n >= i -> loop (acc + s) (n – i) list|
|| x::xs -> loop acc n xs|
|loop "" iVal vals|
|let fromRoman sVal =|
|let rec loop acc (str : string) list =|
|match list with|
||  -> acc|
|| (i, s)::xs when str.StartsWith(s) -> loop (acc + i) (str.Substring(s.Length)) list|
|| x::xs -> loop acc str xs|
|loop 0 sVal vals|
I endeavor to explain for those not yet converted to the F# happy-path.
The first lines (1-3), declares a value (“vals”) as a list of tuples. Those tuples consist of a number, and it’s equivalent textual value in the Roman Numeral form. The ordering is important, as it goes from largest numerical value to smallest in the list.
Both functions defined here (toRoman and fromRoman) are very similarly designed. They start with a single parameter, and then define and internal looping function that actually creates the resulting value. Then they call that internal function with an “empty” accumulator, the initial parameter value, and the vals list declared above. If you run a simple test in FSI with these functions you should easily get some good results:
> toRoman 54;; val it : string = "LIV" > toRoman 2017;; val it : string = "MMXVII" > fromRoman "MMXVII";; val it : int = 2017
Internal loops with an accumulator are common in functional code. Our loops use the “acc” (aka, accumulator) parameter to sum up the matching numbers in the fromRoman function, and to build the resulting string in the toRoman function. The internal loop matches the incoming list, and compares it against 3 possible options. The first possible match is against an empty list. If the list passed in to the loop function is empty, it simply returns the acc value. The second match is a list with the head of the element deconstructed as a tuple (i, s), filtering when the “n” parameter (of the loop function) is larger than (or equal to) the “i” value in the tuple. In that case, we simply call the loop function again, appending the s value to the acc parameter, subtracting the i value from the n parameter, and passing the same list into the function again. Finally the third option is when it’s just a simple list object that has a head and a tail (the match order counts) and it passes the existing acc and n parameter values to the loop function with the tail of the list.
When I pass in 54 into the toRoman function, the first thing that happens in that function is:
1) The loop function is defined and then called, with “” as the acc parameter, 54 as the n parameter and the vals list [(1000, “M”)…(1, “I”)] as the list parameter.
2) The loop executes, matching against the list parameter.
3) The list is not empty, so it skips the first match.
4) The first value in the list (1000, “M”) is capable of being represented as a tuple (i, s), and the list itself matches the cons operator (“::“) as well, but the “when” setting n >= i does NOT match, because the n parameter is 54 and the i is 1000, so it skips the second match.
* Reread and, make sure you understand this part, as it’s the critical point of the function.
5) The final option matches, and calls loop again, this time with parameters: acc = “”; n = 54, and list = [(900, “CM”)…(1, “I”)]
6) The steps 3-5 execute again. The head of the list is compared, until eventually the “n” parameter is larger or equal to the corresponding “i” (from step 4).
(900, “CM”) -> Nope
(500, “D”) -> Nada
(400, “CD”) -> No Dice
(50, “L”) -> That’s a match!
7) Since that match occurred, we STILL loop through the execution, but now we modify our parameters a bit. Instead of the plain old acc, now we apply a function to the acc to accumulate the value; acc is no longer “”, it is now “” + “L”. The variable n is modified as well. It is no longer 54, it becomes 4 (54 – 50). The list value stays the same [(50,”L”)…(1, “I”)].
8) The new values are now matched, and the process continues. The list is still not empty, and the new n value (4) is clearly less than 50 (the first element in the list), so we move on through the list, in the same way we did before but against the new n value (4):
(50, “L”) -> Nope
(40, “XL”) -> Negative
(10, “X”) -> No Dice
(9, “IX”) -> Sorry
(5, “V”) -> Not quite
(4, “IV”) -> Yep!
9) The result updates acc to “LIV”, and n to 0. We loop through the remaining 2 items in the list (4, “IV”) and (1, “I”) and get to the last possible match , the empty list. The last match returns the acc passed in (“LIV”). That becomes our final value, and is the result of our toRoman function.
The differences between fromRoman and toRoman are simply related to the data types involved. String “subtraction” doesn’t work with a simple minus sign (at least, not without defining a new infix operator), so instead it’s “str.Substring(s.Length).” String comparison doesn’t work with the >= operator, so str.StartsWith(s) was recruited to do the job. Everything else largely works the same, the accumulator accumulates the values while the function executes and the str variable is “decremented”, until the list is exhausted.
There are some clear flaws in my implementations here, and I accept them for what they are. Firstly, in the toRoman function, negative value parameters all return an empty string. I don’t remember my mathematics history there, and I am not sure if they even actually had the concept of negative numbers back then. If you are a history buff, and know the answer, please comment and let me know. Secondly, the fromRoman function isn’t validating the incoming text. You could submit something things like “CDCDCD”, and it would return 1200, even though the input is clearly wrong. Finally, when you put in an exceptionally large number into toRoman, you can end up with a LOT of “M”s, simply because the vals list doesn’t contain enough “domain knowledge” about larger numbers.
Still, it largely works, so for a Fun Friday, I’m happy with it. Enjoy!