Roman Numerals Kata

In the Ruby world TDD is often demonstrated with a kata of converting decimal numbers into roman numerals. Here’s my take on it with a gentle slope in introducing complexity, with a stepwise discovery of the concepts of…

We start with the minimal implementation for 1.

class Integer
  def to_roman
    "I"
  end
end

For 2 we discover our first concept: repetition.

class Integer
  def to_roman
    "I" * self
  end
end

That works for 3 as well, then we add a special case for 4. We decide, we can live with that.

class Integer
  def to_roman
    return "IV" if self == 4
    "I" * self
  end
end

This will break again, at 5. To go green asap, we quickly add yet another special case.

class Integer
  def to_roman
    return "V" if self == 5
    return "IV" if self == 4
    "I" * self
  end
end

And then we refactor, thereby discovering the second core concept: transliteration.

class Integer
  NUMERALS = [[5, "V"], [4, "IV"], [1, "I"]]
 
  def to_roman
    NUMERALS.each do |decimal, roman|
      count = self / decimal
      return roman * count if count > 0
    end
  end
end

For 6, again, we simply add a special case and move on.

class Integer
  NUMERALS = [[6, "VI"], [5, "V"], [4, "IV"], [1, "I"]]
  
  def to_roman
    NUMERALS.each do |decimal, roman|
      count = self / decimal
      return roman * count if count > 0
    end
  end
end

At 7, breaking again, we must get green quickly, even if it comes at the price of a second special case.

class Integer
  NUMERALS = [[7, "VII"], [6, "VI"], [5, "V"], [4, "IV"], [1, "I"]]
  def to_roman
    NUMERALS.each do |decimal, roman|
      count = self / decimal
      return roman * count if count > 0
    end
  end
end

And because this is now ripe to refactor, we introduce the third concept: concatenation.

class Integer
  NUMERALS = [[5, "V"], [4, "IV"], [1, "I"]]
  def to_roman
    result, remainder = "", self
    NUMERALS.each do |decimal, roman|
      count, remainder = remainder.divmod decimal
      result << roman * count
    end
    result
  end
end

Working with the translation table we can complete the converter, also adding error handling.

class Integer
  NUMERALS = [
    [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"]
  ]
  
  def to_roman
    raise "There's no such a roman number" if self < 1 || self > 3999
    result, remainder = "", self
    NUMERALS.each do |decimal, roman|
      count, remainder = remainder.divmod decimal
      result << roman * count
    end
    result
  end
end

The TDD approach guided us separating the discovery of the core concepts – repetition, transliteration and concatenation – into distinct steps. Nice.

However.

There’s still quite some fugly duplication going on in the translation table, indicating that we’ve ignored the fourth concept lurking behind the remaining double-letter values: prefixing. Whether the additional algorithmic complexity is worth the elimination of conceptual duplication is yet to be seen…