Yesterday was a big day. I graduated from the FlatironSchool 12-week program (yay! applause).
It’s been an intense race to pack in as much knowledge as possible in a short time-frame. As a parting gift, we were given an appropriate signifier that we are now versatile web application developers: a swiss army knife.
In recent weeks, it’s been difficult not to notice time ticking away against our task list:
– Absorb knowledge
– Complete web application for ScienceFair
– Absorb knowledge
– Practice ruby problems in preparation for Interviews
– Absorb knowledge
– Ask for 11th-hour lectures to make sure we understand topic xyz
– Absorb knowledge
In the end, as a coping mechanism, my peers and I decided that it was best to look at graduation not as the end of the road but as a new beginning. I’m happy to say that I’ve made it through the program. I’m also happy to report that I’ve largely absorbed most of my newfound knowledge.
The harrowing schedule has made it difficult for many of us to keep up with our blogs, but I’m ready and raring to go. So, I’ve decided to start a new segment called ‘Ruby & Me’ to a) force me to review my notes, b) test my understanding of basic concepts, and c) document my thought process as I work through problems. The immediate result may not be pretty, but this is what the result looks like short of refactoring. In the words of our Dean, Avi Flombaum: Make it work – Make it right – Make it fast. Then and only then: refactor.
Problem: Convert roman numerals to arabic
I’ve learned the hard way that it’s paralyzing to look too far ahead. Small successes are productive and feel great. So, I start with the following pseudo-code:
# Convert roman numerals to arabic:
# I, V, X, L, C, D, M
# NumTranslator.new("V") return arabic numeral value 5
My first goal is to create a class in which I can instantiate a new object and successfully pass my roman numeral between methods. By the way, I’ve also learned the hard way that too much coding without testing is a recipe for disaster. The last thing I want is to get bogged down debugging my code, but I’ve done it before… a lot. So, let’s see how this works out:
class NumTranslator
def initialize(roman_numeral)
@numeral = roman_numeral
end
def translator
@numeral = "V"
end
end
puts NumTranslator.new("V").translator
return value #=> V
My first test passes. Awesome! Now I want to setup a simple translator that deals with all single-digit roman numerals:
def translator
case @numeral
when "I" then 1
when "V" then 5
when "X" then 10
when "L" then 50
when "C" then 100
when "D" then 500
when "M" then 1000
else
puts "Please enter a roman numeral"
end
end
puts NumTranslator.new("V").translator
return value #=> 5
Second test passes. Cool. Now I want to make sure that my full translation table works so I modify my call to accept user input:
puts "Enter roman numeral to convert to arabic numeral: "
print NumTranslator.new(gets.chomp).translator
At this point I realize that I should probably help guard my entries from my lightning fingers.
def initialize(roman_numeral)
@numeral = roman_numeral.upcase
end
Sweet, now I can mistakenly enter a lowercase letter and it will automatically be upcased.
Now comes the tough part. Translation is easy at a single digit level, but if I enter a multiple digit roman numeral, I get the following response: “Please enter a roman numeral”. It’s time to alter my method so that I can deal with multiple letter roman numerals. I know that adjacent numerals sum to a total value e.g. “III” = 3. I also know that there are exceptions but I choose to ignore those for now.
Since each digit within a roman numeral translates to a value, I decide to update my translator method to split the input string and see if I can get “III” to return 3. I decide to use the collect method because I want to add each letter to an array. This will allow me to iterate through and add up all the values.
def translator
@numeral = @numeral.split('').collect do |letter|
case letter
when "I" then 1
when "V" then 5
when "X" then 10
when "L" then 50
when "C" then 100
when "D" then 500
when "M" then 1000
else
puts "Please enter a roman numeral"
end
end
@numeral.inject(:+)
end
(...)
gets.chomp #=> "III"
return value #=> 3
Excellent! It works. It’s time to get happy:
gets.chomp #=> "XI"
return value #=> 11
Yippee!
gets.chomp #=> "LV"
return value #=> 55
Hoorah!
gets.chomp #=> "XX"
return value #=> 20
Woopdeedoo!
gets.chomp #=> "IV"
return value #=> 6
Sigh. Ok, enough prolonging the inevitable. Good thing I’ve learned to manipulate my expectations: Yay! Yay! Yay! Waaaah!
Time for more pseudo-code:
# Translator method assumes each single-digit character is unique
# It splits and translates each character into a number, and adds it to an array
# Exceptions must be converted to a single-digit string and added back to the roman numeral
# Exceptions: "IV" = "4" / "IX" = "9" / "XL" = "F" (forty) / "XC" = "N" (ninety)
The translator method is already crazy long, so it seems appropriate to create a separate method to detect exceptions. I decide to start with “IV” and “IX” to keep things simple.
def detect_exception
if @numeral.include?("IV")
@numeral = @numeral.split("IV").push("4").inject(:+)
elsif @numeral.include?("IX")
@numeral = @numeral.split("IX").push("9").inject(:+)
else
@numeral
end
end
Let’s test this real quick:
puts "Enter roman numeral to convert to arabic numeral: "
print NumTranslator.new(gets.chomp).detect_exception
gets.chomp #=> "XXIV"
return value #=> "XX4"
Cool, that works.
puts "Enter roman numeral to convert to arabic numeral: "
print NumTranslator.new(gets.chomp).detect_exception
gets.chomp #=> "LXIX"
return value #=> "LX9"
Awesome. Now I need to figure out how to expand this to include the other exceptions. I find that I can’t just add these as additional elsif statements because there seems to be confusion with overlapping numerals, so I try to nest them:
def detect_exception
if @numeral.include?("IV")
@numeral = @numeral.split("IV").push("4").inject(:+)
if @numeral.include?("XL")
@numeral = @numeral.split("XL").push("F").inject(:+)
elsif @numeral.include?("XC")
@numeral = @numeral.split("XC").push("N").inject(:+)
else
@numeral
end
elsif @numeral.include?("IX")
@numeral = @numeral.split("IX").push("9").inject(:+)
if @numeral.include?("XL")
@numeral = @numeral.split("XL").push("F").inject(:+)
elsif @numeral.include?("XC")
@numeral = @numeral.split("XC").push("N").inject(:+)
else
@numeral
end
elsif @numeral.include?("XL")
@numeral = @numeral.split("XL").push("F").inject(:+)
elsif @numeral.include?("XC")
@numeral = @numeral.split("XC").push("N").inject(:+)
else
@numeral
end
end
It’s butt ugly, but it works. Only a single line needs to be added to the translator method to call this new method to detect exceptions:
def translator
@numeral = self.detect_exception
@numeral = @numeral.split('').collect do |letter|
(...)
end
end
Excellent. Now we just need to test that everything works:
puts "Enter roman numeral to convert to arabic numeral: "
print NumTranslator.new(gets.chomp).translator
gets.chomp #=> "MMMDXCLXIX"
return value #=> "3669"
I can hardly contain myself. I run about fifteen different number combinations to make sure that everything works. Sweet! Hopefully, I didn’t leave out any exceptions.
UPDATE: Special thanks to Ed for pointing out a problem with my nested conditions under detect_exception, and for suggesting saving on real estate by using a hashmap instead of a case statement.
Here’s the final code:
class NumTranslator
def initialize(roman_numeral)
@numeral = roman_numeral.upcase
end
def detect_exception
if @numeral.include?("IV")
@numeral = @numeral.split("IV").push("4").inject(:+)
if @numeral.include?("XL")
@numeral = @numeral.split("XL").push("F").inject(:+)
elsif @numeral.include?("XC")
@numeral = @numeral.split("XC").push("N").inject(:+)
else
@numeral
end
elsif @numeral.include?("IX")
@numeral = @numeral.split("IX").push("9").inject(:+)
if @numeral.include?("XL")
@numeral = @numeral.split("XL").push("F").inject(:+)
elsif @numeral.include?("XC")
@numeral = @numeral.split("XC").push("N").inject(:+)
else
@numeral
end
elsif @numeral.include?("XL")
@numeral = @numeral.split("XL").push("F").inject(:+)
elsif @numeral.include?("XC")
@numeral = @numeral.split("XC").push("N").inject(:+)
else
@numeral
end
end
def translator
hashmap = {"I"=>1,"4"=>4,"V"=>5,"9"=>9,"X"=>10,"F"=>40,"L"=>50,"N"=>90,"C"=>100,"D"=>500,"M"=>1000}
@numeral = self.detect_exception
@numeral = @numeral.split('').collect do |numeral|
hashmap[numeral]
end
@numeral.inject(:+)
end
end
puts "Enter roman numeral to convert to arabic numeral: "
puts NumTranslator.new(gets.chomp).translator