The Roman Numerals Kata, Enhanced

In the Ruby world TDD is often demonstrated with a kata of converting decimal numbers into roman numerals. For example, Enrique Comba Riepenhausen has a beautifully recorded one at softwarecraftsmanship.org. When preparing a Unit Testing Workshop with a TDD demonstration I also chose the converter and scripted a version. It was then that I realised, one can reduce the complexity explosion (at the step where pretty much the entire logic gets entered) into a more gradual progression. Here's my script demonstrating the gradual introduction of the concepts of…

  1. repetition
  2. lookup
  3. aggregation

We start with the minimal implementation for 1.

class Fixnum
  def to_roman
    "I"
  end
end

For 2 we introduce the first concept: repetition.

class Fixnum
  def to_roman
    "I" * self
  end
end

It will work for 3, then we add a special case for 4. We decide, we can live with that.

class Fixnum
  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 Fixnum
  def to_roman
    return "V" if self == 5
    return "IV" if self == 4
    "I" * self
  end
end

And then we refactor, thereby introducing the second core concept: lookup. (Note, that in the classic version this step also introduces aggregation, which is not required yet.)

class Fixnum
  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 Fixnum
  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 Fixnum
  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: aggregation.

class Fixnum
  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

So this is where the classic version of the kata stops (save for completing the translation table and adding error handling). Here, then, is the final solution using this approach.

class Fixnum
  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

And in this script, we separated the (discovery and) introduction of the three core concepts - repetition, lookup and aggregation into distinct steps. Nice.

Except.

There's still quite a bit of fugly duplication going on in the translation table, meaning, we've ignored the fourth concept lurking behind all those repeated values: prefixing. Feel free to have a go at that…

stray_words/roman_numerals_kata_enhanced.txt · Last modified: 2015.01.19 12:08 by infinitary