Roman Numerals: part II

This follows on from Roman Numerals: part I.

Last time I had managed to convert these numbers into Roman numerals:

   1 => I
   2 => II
  10 => X
  20 => XX
 100 => C
 200 => CC
1000 => M  
2000 => MM  
  11 => XI
3012 => MMMXII  

My full implementation at this point:

private val powersOfTen = listOf(1000, 100, 10, 1)

fun Int.toRoman(): String {  
    val placeValues = toDecimalPlaceValues(this)
    return placeValues.map {
        it.magnitude.toRomanDigit().repeat(it.multiplier)
    }.joinToString(separator = "")
}

fun toDecimalPlaceValues(number: Int): List<PlaceValue> {  
    if (number == 0) {
        return emptyList()
    }
    val largestPowerNeeded = powersOfTen.find { number >= it } ?: 1
    if (largestPowerNeeded == 1) {
        return listOf(PlaceValue(multiplier = number, magnitude = 1))
    } else {
        val multiplier = number / largestPowerNeeded
        val remainder = number % largestPowerNeeded
        return listOf(PlaceValue(multiplier,
                                 magnitude = largestPowerNeeded))
                + toDecimalPlaceValues(remainder)
    }
}

private fun Int.toRomanDigit(): String = when(this) {  
    1000 -> "M"
    100 -> "C"
    10 -> "X"
    1 -> "I"
    else -> throw RomanNumeralException(
                "No mapping to symbol for ${this}")
}

internal class RomanNumeralException(message: String) :  
        RuntimeException(message)

data class PlaceValue(val multiplier: Int, val magnitude: Int) {  
    override fun toString(): String = "$multiplier*$magnitude"
}

In general, my solution could handle any number whose decimal representation used only the digits 0–3 (and not exceeding 3333).

This post describes my steps to completing the kata, so as to handle the remaining digits.

Five, fifty, five hundred

I added my test for 5 ⇒ V. The toDecimalPlaceValues function already worked for all decimal digits so I didn’t need to change that. I just needed to add an entry 5 -> "V" in toRomanDigit conversion and then improve the function I mapped over the PlaceValues:

fun Int.toRoman(): String {  
    val placeValues = toDecimalPlaceValues(this)
    return placeValues.map {
        if (it.multiplier == 5) {
            5.toRomanDigit()
        } else {
            it.magnitude.toRomanDigit().repeat(it.multiplier)
        }
    }.joinToString(separator = "")
}

Git commit 3d54827

Obviously, this would only work for 5 in the units place, not for 5 tens or 5 hundreds. I had to add a mapping 50 -> "L" in toRomanDigit, but then the only change I needed to make inside the PlaceValue mapping function was from this:

if (it.multiplier == 5) {  
    5.toRomanDigit()
}

to this:

if (it.multiplier == 5) {  
    (5 * it.magnitude).toRomanDigit()
}

Git commit 77b05ee

500 ⇒ D was as simple as adding a mapping 500 -> "D" in toRomanDigit.

Git commit 1023ff9

At this point I added a (hopefully) redundant test of 1555 ⇒ MDLV just to reassure myself before moving onto the more tricky issue of 4, 40 and 400.

Git commit a876219

Four, forty, four hundred

So, fives turned out to be straightforward — my investment in mapping to PlaceValues was definitely paying off — but fours might be a bit more fiddly?

Rather than add another if/else to the place values mapping function, I used another when-expression:

return placeValues.map {  
    fun one() = it.magnitude.toRomanDigit()
    fun five() = (it.magnitude * 5).toRomanDigit()
    when (it.multiplier) {
        in 1..3 -> one().repeat(it.multiplier) 
        4 -> one() + five()
        5 -> five()
        else -> ""
    }
}.joinToString(separator = "")

At first I defined val one and val five but when mapping a PlaceValue with magnitude of 1000 it blew up because I don’t have a Roman digit mapping for 5000; the kata doesn’t provide one1. So instead I defined a couple of local functions2.

Not too hard, after all!

Git commit add596c

I was fairly sure this would work for 40 and 400 as well, so added a test for 444 ⇒ CDXLIV, which passed.

Git commit d8060c6

Six, sixty, six hundred

I was definitely on the home straight now, and went directly to a test for 666 ⇒ DCLXVI. Implementation was obvious:

return placeValues.map {  
    fun one() = it.magnitude.toRomanDigit()
    fun five() = (it.magnitude * 5).toRomanDigit()
    when (it.multiplier) {
        in 1..3 -> one().repeat(it.multiplier) 
        4 -> one() + five()
        5 -> five()
        6 -> five() + one()  // added this line
        else -> ""
    }
}.joinToString(separator = "")

I was still keeping an eye out for opportunities to refactor, but couldn’t spot any.

Git commit 5f0a68b

Seven, seventy, seven hundred

Also really easy at this point! The test was for 777 ⇒ DCCLXXVII.

return placeValues.map {  
    fun one() = it.magnitude.toRomanDigit()
    fun five() = (it.magnitude * 5).toRomanDigit()
    when (it.multiplier) {
        in 1..3 -> one().repeat(it.multiplier) 
        4 -> one() + five()
        5 -> five()
        6 -> five() + one()
        7 -> five() + one() + one()  // added this line
        else -> ""
    }
}.joinToString(separator = "")

Git commit 35411e9

Eight, eighty, eight hundred

So nearly there… a test for 888 ⇒ DCCCLXXXVIII and the obvious implementation…

return placeValues.map {  
    fun one() = it.magnitude.toRomanDigit()
    fun five() = (it.magnitude * 5).toRomanDigit()
    when (it.multiplier) {
        in 1..3 -> one().repeat(it.multiplier) 
        4 -> one() + five()
        5 -> five()
        6 -> five() + one()
        7 -> five() + one() + one()
        8 -> five() + one() + one() + one() // added this line
        else -> ""
    }
}.joinToString(separator = "")

I decided, though, that I could reduce this a little bit.

return placeValues.map {  
    fun one() = it.magnitude.toRomanDigit()
    fun five() = (it.magnitude * 5).toRomanDigit()
    when (it.multiplier) {
        in 1..3 -> one().repeat(it.multiplier) 
        4 -> one() + five()
        5 -> five()
        in 6..8 -> five() + one().repeat(it.multiplier - 5)
        else -> ""
    }
}.joinToString(separator = "")

In retrospect, I think I should have left it alone, as the longer version was more readable.

Git commit 87d0c99

Nine, ninety, nine hundred

Last step! To satisfy 999 ⇒ CMXCIX, I just needed a ten() function and one more case, and I had my final implementation of toRoman():

fun Int.toRoman(): String {  
    val placeValues = toDecimalPlaceValues(this)
    return placeValues.map {
        fun one() = it.magnitude.toRomanDigit()
        fun five() = (it.magnitude * 5).toRomanDigit()
        fun ten() = (it.magnitude * 10).toRomanDigit()
        when (it.multiplier) {
            in 1..3 -> one().repeat(it.multiplier) 
            4 -> one() + five()
            5 -> five()
            in 6..8 -> five() + one().repeat(it.multiplier - 5)
            9 -> one() + ten()
            else -> ""
        }
    }.joinToString(separator = "")
}

Git commit 21534a0

Just for fun…

I added a test for the longest Roman numeral representing a year in the past: 1888 ⇒ MDCCCLXXXVIII.

Fortunately, it passed.

Git commit 0a41b82

Closing thoughts

I really like Kotlin for TDD as the ability just to write a function without an enclosing object suits the nature of a lot of these katas. The syntax isn’t too different from Java, and the functional methods on collections are nice. Optional application of named parameters, such as in PlaceValue(multiplier = 2, magnitude = 100), makes for great clarity.


  1. Apparently, for larger numbers than M, the Romans would draw a line above the digit to indicate it was multiplied by 10, so 5000 would be V with a line above it. This doesn’t come up much these days, as Roman numerals are only really used for dates.

  2. Kotlin has “lazy” properties of classes/objects but not yet lazy local values. It’s on the roadmap.


Image credit: photograph of Cleveland Bridge plaque, Bath by Jaggery is licensed for reuse under CC BY-SA 2.0

Michael Bannister

Read more posts by this author.

London, UK