I’m new to Ruby and to Test Driven Development (TDD). Good thing the latter is helping me learn the former and write better code in the process.
I recently went through the Roman Numeral Kata shown by Jim Weirich here to get more acquainted with TDD. This kata showed me that through TDD I could write pretty, complex (hey, for me this is complex) code without much difficulty.
Let me start with the Roman Numeral Kata challenge: Turn modern numbers into the correct roman numeral. For example, 1 should equal I, 2 = II, 10 = X, 38 = XXXVIII, 63 = LIII, and so on.
As a beginner software engineer, it’s overwhelming at first to try and think of a complete solution. How will I handle 4’s? How will I be able to write clean code without 50 different if statements? Where should I start?
Luckily, TDD makes the answer to the most important question “Where should I start?” so simple. Start at the very beginning.
Start by setting up roman_numeral_converter.rb and roman_numeral_converter_spec.rb in your text editor like so:
roman_numeral_converter_spec.rb
require_relative 'roman_numeral_converter' require 'rspec/given' RSpec::Given.use_natural_assertions describe RomanNumeralConverter do end
roman_numeral_converter.rb
#It's blank!
Then run rspec. You’ll receive the fun error ‘uninitialized constant RomanNumeralConverter’ so go ahead and set that up.
roman_numeral_converter.rb
class RomanNumeralConverter end
Next, add your first test test and try to think of the simplest solution you can to make the test pass:
roman_numeral_converter_spec.rb
describe RomanNumeralConverter do Given (:converter) {RomanNumeralConverter.new} Then {converter.convert(1).should == "I"} end
roman_numeral_converter.rb
class RomanNumeralConverter def convert(n) "I" end end
See… starting with “I” is something I can do. That was easy. Now write a second test that is just a little more complex:
roman_numeral_converter_spec.rb
describe RomanNumeralConverter do Given (:converter) {RomanNumeralConverter.new} Then {converter.convert(1).should == "I"} Then {converter.convert(2).should == "II"} end
roman_numeral_converter.rb
class RomanNumeralConverter def convert(n) "I" * n end end
Now, we’re on a roll! That small line of code will succesfully get us from 1 to 3. Since 4 is a little bit complicated, let’s move on to 5:
roman_numeral_converter_spec.rb
describe RomanNumeralConverter do Given (:converter) {RomanNumeralConverter.new} Then {converter.convert(1).should == "I"} Then {converter.convert(2).should == "II"} Then {converter.convert(5).should == "V"} end
roman_numeral_converter.rb
class RomanNumeralConverter def convert(n) result = "" if n == 5 result = "V" else result = "I" * n end result end end
That looks great, but it won’t really help us get to 6, 7, or 8 so let’s add a little more code:
roman_numeral_converter_spec.rb
describe RomanNumeralConverter do Given (:converter) {RomanNumeralConverter.new} Then {converter.convert(1).should == "I"} Then {converter.convert(2).should == "II"} Then {converter.convert(5).should == "V"} Then {converter.convert(6).should == "VI"} end
roman_numeral_converter.rb
class RomanNumeralConverter def convert(n) result = "" if n >= 5 result << "V" n -= 5 end result << "I" * n result end end
Finally we're starting to get somewhere. It's becoming clear that you have to append the the lower roman numerals to the end of the result. Now let's tackle 10.
roman_numeral_converter_spec.rb
describe RomanNumeralConverter do Given (:converter) {RomanNumeralConverter.new} Then {converter.convert(1).should == "I"} Then {converter.convert(2).should == "II"} Then {converter.convert(5).should == "V"} Then {converter.convert(6).should == "VI"} Then {converter.convert(10).should == "X"} end
roman_numeral_converter.rb
def convert(n) result = "" if n >= 10 result << "X" n -= 10 end if n >= 5 result << "V" n -= 5 end result << "I" * n result end
Okay we are starting to see a pattern by looking at 5 and 10, but I'm not sure we're quite there. If we try to convert 20 we should get "XX", but right now we would get "XVIIIII" so our test would fail. We want to keep adding X's as long as n is greater than 10, right? So this is what that looks like:
roman_numeral_converter_spec.rb
describe RomanNumeralConverter do Given (:converter) {RomanNumeralConverter.new} Then {converter.convert(1).should == "I"} Then {converter.convert(2).should == "II"} Then {converter.convert(5).should == "V"} Then {converter.convert(6).should == "VI"} Then {converter.convert(10).should == "X"} Then {converter.convert(20).should == "XX"} end
roman_numeral_converter.rb
def convert(n) result = "" While n >= 10 result << "X" n -= 10 end if n >= 5 result << "V" n -= 5 end result << "I" * n result end
Woo-hoo! All our tests pass. Now, let's tackle 4 and 9, or "IV" and "IX" by copying our patten for 5.
roman_numeral_converter_spec.rb
describe RomanNumeralConverter do Given (:converter) {RomanNumeralConverter.new} Then {converter.convert(1).should == "I"} Then {converter.convert(2).should == "II"} Then {converter.convert(4).should == "IV"} Then {converter.convert(5).should == "V"} Then {converter.convert(6).should == "VI"} Then {converter.convert(9).should == "IX"} Then {converter.convert(10).should == "X"} Then {converter.convert(20).should == "XX"} end
roman_numeral_converter.rb
def convert(n) result = "" While n >= 10 result << "X" n -= 10 end if n >= 9 result << "IX" n -= 9 end if n >= 5 result << "V" n -= 5 end if n >= 4 result << "IV" n -= 4 end result << "I" * n result end
We now have a working roman numeral converter, but that code is not very pretty. Let's look for patterns and simplify (i.e., refactor). All of our if statements can actually be while statements, similar to the chunk of code that converts 10 to X. If we look even closer it appears that the final piece of code that converts 1 to I can also be turned into a while statement. Then we can just use variables and pass them through. See the final outcome here:
roman_numeral_converter_spec.rb
describe RomanNumeralConverter do Given (:converter) {RomanNumeralConverter.new} Then {converter.convert(1).should == "I"} Then {converter.convert(2).should == "II"} Then {converter.convert(4).should == "IV"} Then {converter.convert(5).should == "V"} Then {converter.convert(6).should == "VI"} Then {converter.convert(9).should == "IX"} Then {converter.convert(10).should == "X"} Then {converter.convert(20).should == "XX"} end
roman_numeral_converter.rb
def convert_to_roman_numeral(n) result = "" numbers = [ [10,"X"], [9,"IX"], [5,"V"], [4,"IV"], [1,"I"] ] numbers.each do |num, letter| while n >= num result << letter n -= num end end result end
Yeah, that just happened. As you can see, the while statement is the same as the one we were using before. Now we are just passing sets of modern numbers and roman numerals through it. We can add more sets in our numbers array to convert higher numbers such as [50, "L"] and [40, "XL"].
I really had no idea how this converter would turn out when I first started. Luckily, I didn't have to spend too much time thinking about it. All I had to do was start testing with the simplest test I could think of and then go from there. And before I knew it, I ended up with a well-written Roman Numeral Converter.
Thanks TDD!