Skip to content

Instantly share code, notes, and snippets.

@jimweirich
Created July 20, 2011 16:37
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jimweirich/1095310 to your computer and use it in GitHub Desktop.
Save jimweirich/1095310 to your computer and use it in GitHub Desktop.
Results from Roman Numeral Calculator Kata at @cincinnatirb
# 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.
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
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
@tiagoefmoraes
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment