Created
July 20, 2011 16:37
-
-
Save jimweirich/1095310 to your computer and use it in GitHub Desktop.
Results from Roman Numeral Calculator Kata at @cincinnatirb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# When I posted the results of the Roman Numeral Calculator kata | |
# earlier this week, I said that I felt that the evolution of the code | |
# through TDD was much more interesting than the final result. Let me | |
# explain. | |
# | |
# First, some background. The goal of this Kata is to produce a | |
# RomanNumeralCalculator object that converts from arabic numbers to | |
# Roman numerals, and from Roman numerals back to arabic. | |
Then { calculate("1").should == "I" } | |
Then { calculate("2").should == "II" } | |
# The code for calculate at that point is: | |
def calculate(string) | |
"I" * string.to_i | |
end | |
# I wanted to skip "4" because I suspected that wasn't the simpliest | |
# case, so the next test I ran was "5". | |
Then { calculate("5").should == "V" } | |
# This forced an if/then/else into the implementation | |
def calculate(string) | |
n = string.to_i | |
if n >= 5 | |
"V" | |
else | |
"I" * string.to_i | |
end | |
end | |
# I knew that the else wasn't going to work, so I picked the next test | |
# to force me to get rid of the else, one that both "V"s and "I"s. | |
Then { calculate("6").should == "VI" } | |
# At this step it is clear that I need to build up the result | |
# incrementally, so I created a result string to accumlate the answer | |
# and built it up from there. | |
def calculate(string) | |
n = string.to_i | |
result = "" | |
if n >= 5 | |
result << "V" | |
n -= 5 | |
end | |
result << "I" * n | |
end | |
# Decision time ... do I go back and pick up the "IV", or do I press on | |
# to "X". I felt I wanted to see more of the big pattern before | |
# handling "4", so let's do "X" next. | |
Then { calculate("10").should == "X" } | |
# Liking the pattern with "5", I mimicked it for the "10". | |
def calculate(string) | |
n = string.to_i | |
result = "" | |
if n >= 10 | |
result << "X" | |
n -= 10 | |
end | |
if n >= 5 | |
result << "V" | |
n -= 5 | |
end | |
result << "I" * n | |
end | |
# But some Roman numerals need multiple "X"s, so the next test should | |
# demonstrate that. | |
Then { calculate("20").should == "XX" } | |
# I really enjoyed this change to handle "XX". I just changed an "if" | |
# into a "while": | |
def calculate(string) | |
n = string.to_i | |
result = "" | |
while n >= 10 | |
result << "X" | |
n -= 10 | |
end | |
if n >= 5 | |
result << "V" | |
n -= 5 | |
end | |
result << "I" * n | |
end | |
# Now that I see the pattern, I think I'm ready to tackle the "IV". | |
Then { calculate("4").should == "IV" } | |
# Results in: | |
def calculate(string) | |
n = string.to_i | |
result = "" | |
while n >= 10 | |
result << "X" | |
n -= 10 | |
end | |
if n >= 5 | |
result << "V" | |
n -= 5 | |
end | |
if n >= 4 | |
result << "IV" | |
n -= 4 | |
end | |
result << "I" * n | |
end | |
# Time for some refactoring. The final piece of code that deals with | |
# the I's is bothering me, so I change it to look more like the 10 | |
# case: | |
def calculate(string) | |
n = string.to_i | |
result = "" | |
while n >= 10 | |
result << "X" | |
n -= 10 | |
end | |
if n >= 5 | |
result << "V" | |
n -= 5 | |
end | |
if n >= 4 | |
result << "IV" | |
n -= 4 | |
end | |
while n >= 1 | |
result << "I" | |
n -= 1 | |
end | |
result | |
end | |
# Why are some of the code snippets using an IF and others use a | |
# WHILE? Obviously because "V" and "IV" are only inserted once if | |
# needed. But since IF is simply a WHILE that's executed once, | |
# changing the IFs to WHILEs reveals a deeper pattern. | |
def calculate(string) | |
n = string.to_i | |
result = "" | |
while n >= 10 | |
result << "X" | |
n -= 10 | |
end | |
while n >= 5 | |
result << "V" | |
n -= 5 | |
end | |
while n >= 4 | |
result << "IV" | |
n -= 4 | |
end | |
while n >= 1 | |
result << "I" | |
n -= 1 | |
end | |
result | |
end | |
# Now it's obvious. The solution I'm driving for is a series of loops | |
# that reduce the number by a certain amount and add the proper Roman | |
# numeral digit to the result string. Since each loop in that series | |
# differs by only the amount and the Roman digit, we can extract that | |
# into a table. | |
ROMAN_REDUCTIONS = [ | |
[10, "X"], | |
[5, "V"], | |
[4, "IV"], | |
[1, "I"], | |
] | |
def calculate(string) | |
n = string.to_i | |
result = "" | |
ROMAN_REDUCTIONS.each do |value, roman_digit| | |
while n >= value | |
result << roman_digit | |
n -= value | |
end | |
end | |
result | |
end | |
# Now the code is essentially done. All we need to do is flesh out | |
# the table (adding appropriate tests as needed, of course). | |
ROMAN_REDUCTIONS = [ | |
[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 calculate(string) | |
n = string.to_i | |
result = "" | |
ROMAN_REDUCTIONS.each do |value, roman_digit| | |
while n >= value | |
result << roman_digit | |
n -= value | |
end | |
end | |
result | |
end | |
# There are around three key insights that really drive this solution. | |
# | |
# (1) That "I" * n was equivalent to the "while" loops. | |
# | |
# By recognizing this, a pattern began to emerge in the solution. | |
# | |
# (2) Moving from IF to WHILE, even for the values that only | |
# strictly needed and "if". | |
# | |
# Without that insight we would have been left with a confusing | |
# mess of alternating if's and while's. By recognizing an IF | |
# is simply a WHILE that runs once, the possibility of a | |
# simpler final solution was secured. | |
# | |
# (3) That the whole series of loops varied only in data, so the | |
# data could be extracted into an array and the loops condensed | |
# into a single loop within a loop. | |
# | |
# I hope you enjoyed this analysis of the Roman Numeral Calculator | |
# kata. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class RomanNumeralCalculator | |
def calculate(numeral) | |
if numeral =~ /^\d+$/ | |
arabic_to_roman(numeral) | |
else | |
roman_to_arabic(numeral) | |
end | |
end | |
private | |
ROMAN_REDUCTIONS = [ | |
["M", 1000], | |
["CM", 900], | |
["D", 500], | |
["CD", 400], | |
["C", 100], | |
["XC", 90], | |
["L", 50], | |
["XL", 40], | |
["X", 10], | |
["IX", 9], | |
["V", 5], | |
["IV", 4], | |
["I", 1]] | |
ROMAN_VALUES = Hash[*ROMAN_REDUCTIONS.flatten] | |
def arabic_to_roman(numeral) | |
n = numeral.to_i | |
result = "" | |
ROMAN_REDUCTIONS.each do |roman_digit, val| | |
while n >= val | |
result << roman_digit | |
n -= val | |
end | |
end | |
result | |
end | |
def roman_to_arabic(numeral) | |
result = 0 | |
numerals = numeral.split(//) | |
while !numerals.empty? | |
a, b = numerals | |
if value_of(a) >= value_of(b) | |
result += value_of(a) | |
else | |
result += value_of(b) - value_of(a) | |
numerals.shift | |
end | |
numerals.shift | |
end | |
result.to_s | |
end | |
def value_of(roman_digit) | |
ROMAN_VALUES[roman_digit] || 0 | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'rspec/given' | |
require 'roman_numeral_calculator' | |
describe RomanNumeralCalculator do | |
Given(:calculator) { RomanNumeralCalculator.new } | |
def calculate(n) | |
calculator.calculate(n) | |
end | |
describe "converting Roman to Arabic" do | |
describe "with single digits" do | |
Then { calculate("I").should == "1" } | |
Then { calculate("V").should == "5" } | |
Then { calculate("X").should == "10" } | |
Then { calculate("L").should == "50" } | |
Then { calculate("C").should == "100" } | |
Then { calculate("D").should == "500" } | |
Then { calculate("M").should == "1000" } | |
end | |
describe "with multiple digits" do | |
Then { calculate("II").should == "2" } | |
Then { calculate("VIII").should == "8" } | |
Then { calculate("MMMCCCXXXIII").should == "3333" } | |
end | |
describe "with inverted ordered digits" do | |
Then { calculate("IV").should == "4" } | |
Then { calculate("IX").should == "9" } | |
Then { calculate("XL").should == "40" } | |
Then { calculate("XC").should == "90" } | |
Then { calculate("CD").should == "400" } | |
Then { calculate("CM").should == "900" } | |
end | |
describe "with the largest number" do | |
Then { calculate("MMMCMXCIX").should == "3999" } | |
end | |
end | |
describe "converting arabic to roman" do | |
Then { calculate("1").should == "I" } | |
Then { calculate("2").should == "II" } | |
Then { calculate("3").should == "III" } | |
Then { calculate("4").should == "IV" } | |
Then { calculate("5").should == "V" } | |
Then { calculate("6").should == "VI" } | |
Then { calculate("9").should == "IX" } | |
Then { calculate("10").should == "X" } | |
Then { calculate("30").should == "XXX" } | |
Then { calculate("40").should == "XL" } | |
Then { calculate("50").should == "L" } | |
Then { calculate("90").should == "XC" } | |
Then { calculate("100").should == "C" } | |
Then { calculate("400").should == "CD" } | |
Then { calculate("900").should == "CM" } | |
Then { calculate("1000").should == "M" } | |
Then { calculate("3999").should == "MMMCMXCIX" } | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Video here: http://vimeo.com/33841375