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…

**repetition****lookup****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