Convert arabic to roman

21 views
Skip to first unread message

Piotr 'Qertoip' Włodarek

unread,
Dec 25, 2009, 6:13:36 PM12/25/09
to Clojure
What is the most clear and idiomatic way to convert arabic numbers to
roman notation? My take:

(def roman
(hash-map
1 "I",
4 "IV",
5 "V",
9 "IX",
10 "X",
40 "XL",
50 "L",
90 "XC",
100 "C",
400 "CD",
500 "D",
900 "CM",
1000 "M"
)
)

(defn to-roman
([n]
(to-roman n 1000))
([n divisor]
(if (zero? divisor)
""
(let [times (quot n divisor)
large-component (* times divisor)
small-component (- n large-component)]
(str
(if (roman large-component)
(roman large-component)
(apply str (repeat times (roman divisor))))
(to-roman small-component (quot divisor 10)))))))

(to-roman 3991)

=> "MMMCMXCI"

I am aware of tail recursion problem in JVM but in this particular
case it's very shallow so that shouldn't be an issue.

Mark Engelberg

unread,
Dec 25, 2009, 9:46:28 PM12/25/09
to clo...@googlegroups.com
Structurally, I'd say your program is just fine. But, it did take me
a while to convince myself that your program worked, and understand
why, so I'd say it is less clear than it could be.

When striving for clarity, it is essential to:
1. Break your program into small, easily understandable functions.
2. Include a documentation string for each function which states the
type of inputs and output, and includes a statement of purpose.
3. Include some examples (or better yet, use test cases and the new
pre/post conditions).

I reworked your example in a way that I believe to be more clear.
I'll leave it to other readers to judge:

(def roman
(sorted-map


1 "I",
4 "IV",
5 "V",
9 "IX",
10 "X",
40 "XL",
50 "L",
90 "XC",
100 "C",
400 "CD",
500 "D",
900 "CM",
1000 "M"
)
)

;; Terminology used in this program:
;; A "roman number" is one of the possible keys in the above roman map.
;; A "roman numeral" is a roman string representation of a positive integer.

(defn largest-roman-number-less-than-or-equal-to
"Takes a positive number n and returns the largest roman number less
than or equal to n"
[n]
(last (take-while #(<= % n) (keys roman))))

; Examples:
; (largest-roman-number-less-than-or-equal-to 432) -> 400
; (largest-roman-number-less-than-or-equal-to 57) -> 50

(defn seq-of-roman-numbers
"Takes a positive number n and returns a sequence of roman numbers,
in descending order, that add up to n"
[n]
(if (zero? n) []
(let [first-roman-number (largest-roman-number-less-than-or-equal-to n)]
(cons first-roman-number (seq-of-roman-numbers (- n
first-roman-number))))))

; Examples:
; (seq-of-roman-numbers 432) -> '(400 10 10 10 1 1)
; (seq-of-roman-numbers 7) -> '(5 1 1)

(defn roman-numeral
"Takes a positive number n and returns the roman numeral representation of n"
[n]
(apply str (map roman (seq-of-roman-numbers n))))

; Examples:
; (roman-numeral 432) -> "CDXXXII"
; (roman-numeral 7) -> "VII"


---------------------------------------

My version uses examples in comments, but if you're running Clojure
1.1 or newer, I recommend experimenting with the new test library, and
with the pre/post conditions.

Timothy Pratley

unread,
Dec 26, 2009, 7:36:37 AM12/26/09
to Clojure
Actually - the original version doesn't work:
user=> (to-roman 7)
"IIIIIII"
Is not quite right...

Whereas your version gives the correct output:
user=> (roman-numeral 7)
"VII"

Your version is very clear. It is not necessary to check if the
remaining value is greater than any of the previously found large
values. For such a trivially small problem space this is a premature
optimisation to worry about. If someone really needed to squeeze CPU
cycles out of a roman number converter I'd be quite surprised!

However for the sake of comparison I thought about a strictly
descending approach:
; order largest to smallest
(def r (sorted-map-by >


1 "I",
4 "IV",
5 "V",
9 "IX",
10 "X",
40 "XL",
50 "L",
90 "XC",
100 "C",
400 "CD",
500 "D",
900 "CM",
1000 "M"))

(defn tim-roman
[n]
(apply str
(loop [todo n, rs r, sofar ()]
(let [roman (first rs)
mag (key roman)
left (rest rs)
times (quot todo mag)
chars (concat sofar (repeat times (val roman)))]
(if (seq left)
(recur (if times (mod todo mag) todo)
left
chars)
chars)))))

Arrrggghh ugly - not worth it!!! I found it frustrating that I
couldn't think of a higher order function for this sort of traversal.

Interestingly I stumbled across this cute way of using subtractors to
avoid hard coding IV etc:
http://rosettacode.org/wiki/Roman_Numerals#Ruby


Regards,
Tim.

Timothy Pratley

unread,
Dec 26, 2009, 8:03:58 AM12/26/09
to Clojure
And also from that site the Haskell version is pretty neat though
somewhat esoteric!

(defn digit [x y z k]
(condp = k
1 [x]
2 [x x]
3 [x x x]
4 [x y]
5 [y]
6 [y x]
7 [y x x]
8 [y x x x]
9 [x z]))
(defn tim-roman
[n]
(cond
(= n 0) ""
(>= n 1000) (cons \M (tim-roman (- n 1000)))
(>= n 100) (concat (digit \C \D \M (quot n 100)) (tim-roman (mod n
100)))
(>= n 10) (concat (digit \X \L \C (quot n 10)) (tim-roman (mod n
10)))
:else (digit \I \V \X n)))
(defn str-roman
[n]
(apply str (tim-roman n)))

Stephen C. Gilardi

unread,
Dec 26, 2009, 10:41:17 AM12/26/09
to clo...@googlegroups.com

On Dec 25, 2009, at 6:13 PM, Piotr 'Qertoip' Włodarek wrote:

> What is the most clear and idiomatic way to convert arabic numbers to roman notation?

Though it may miss the true goal of the exercise, there's also this option:

user=> (clojure.contrib.pprint/cl-format nil "~@r" 3991)
"MMMCMXCI"

which is perhaps not as clear as one might like, but is very flexible, very likely to give correct results, and leverages existing capability well.

--Steve

Piotr 'Qertoip' Włodarek

unread,
Dec 26, 2009, 2:32:34 PM12/26/09
to Clojure
On Dec 26, 3:46 am, Mark Engelberg <mark.engelb...@gmail.com> wrote:
> I reworked your example in a way that I believe to be more clear.
> I'll leave it to other readers to judge:

This is awesome, thank you.

Reply all
Reply to author
Forward
0 new messages