class Converter
NUMERALS = { 50 => "L", 40 => "XL", 10 => "X", 9 => "IX", 5 => "V", 4 => "IV", 1 => "I" }
def convert(number)
return NUMERALS[number] if NUMERALS[number]
to_roman(number)
end
private
def to_roman(number)
result = ""
NUMERALS.each do |value, numeral|
while number >= value
result += numeral
number -= value
end
end
result
end
end
Notice the 40, 9, and 4. These aren't unique numerals, rather they are pairs that represent special subtraction rules. Doing this gets my tests to pass and solves the problem, but I wanted to see if I could come up with a way that better represented how roman numerals work.
Looking for inspiration, I asked twitter and google for some help, but everything I found either used this technique or did some magic number substitution (YUCK). So I set out to find my own solution. Notice I am not professing I set out to find a better solution, only my own, alternative solution.
I won't be showing all of the tests as we go along, but the entire solution was test driven with RSpec. You can review the history and see the tests on github.
Pairing Numerals
After a bit of thought, it occurred to me that I could "pair" numerals. A numeral could be assigned a pair. A numeral and it's pair make a new numeral pair through simple subtraction. For example, five is paired with one. Five's pair_numeral is now 4 with a roman value of "IV".The numeral class:
class Numeral
attr_accessor :arabic
attr_accessor :roman
attr_accessor :pair
def initialize(*args)
if args[0] == args[0].to_i
@roman = args[1]
@arabic = args[0]
else
@roman = args[0]
@arabic = args[1]
end
end
def pair_numeral
return Numeral.new(@arabic - @pair.arabic, @pair.roman + roman) if @pair
nil
end
def ==(compare_to)
return @arabic = compare_to.arabic && @roman == compare_to.roman
end
end
I thought later that I don't really need to pass in parameters in variable order and if I do, I could just use a hash, but I didn't get around to cleaning that one up.
The converter class:
require 'numeral'
class Converter
NUMERALS = { 1000 => Numeral.new(1000,"M"), 500 => Numeral.new(500,"D"), 100 => Numeral.new(100,"C"), 50 => Numeral.new(50,"L"), 10 => Numeral.new(10,"X"), 5 => Numeral.new(5,"V"), 1 => Numeral.new(1,"I") }
def initialize
@result = ""
NUMERALS[5].pair = NUMERALS[1]
NUMERALS[10].pair = NUMERALS[1]
NUMERALS[50].pair = NUMERALS[10]
NUMERALS[100].pair = NUMERALS[10]
NUMERALS[500].pair = NUMERALS[100]
NUMERALS[1000].pair = NUMERALS[100]
end
def convert(number)
return NUMERALS[number].roman if NUMERALS[number]
to_roman(number)
@result
end
private
def to_roman(number)
NUMERALS.each do |value, numeral|
number = roman_reduce(number, numeral)
number = roman_reduce(number, numeral.pair_numeral) if numeral.pair_numeral
end
end
def roman_reduce(number, numeral)
while number >= numeral.arabic
@result += numeral.roman
number -= numeral.arabic
end
number
end
end
This isn't bad, I suppose. Of course, there's a lot more code than the original solution, but it better represents how roman numerals work. Or does it?
The name pair doesn't really make sense. And while I got rid of the special cases in my NUMERALS hash, I call each of them out in the converter class anyway.
I asked friends on twitter to take a look and give me feedback. It was suggested that I change the names "pair" and "pair_numeral" to something else that better expresses what's really happening. So I changed "pair" to "subtrahend" and "pair_numeral" to "difference".
The numeral class:
class Numeral
attr_accessor :arabic
attr_accessor :roman
attr_accessor :subtrahend
def initialize(*args)
if args[0] == args[0].to_i
@roman = args[1]
@arabic = args[0]
else
@roman = args[0]
@arabic = args[1]
end
end
def difference
return Numeral.new(@arabic - @subtrahend.arabic, @subtrahend.roman + roman) if @subtrahend
nil
end
def ==(compare_to)
return @arabic = compare_to.arabic && @roman == compare_to.roman
end
end
The converter class:
require 'numeral'
class Converter
NUMERALS = { 1000 => Numeral.new(1000,"M"), 500 => Numeral.new(500,"D"), 100 => Numeral.new(100,"C"), 50 => Numeral.new(50,"L"), 10 => Numeral.new(10,"X"), 5 => Numeral.new(5,"V"), 1 => Numeral.new(1,"I") }
def initialize
@result = ""
NUMERALS[5].subtrahend = NUMERALS[1]
NUMERALS[10].subtrahend = NUMERALS[1]
NUMERALS[50].subtrahend = NUMERALS[10]
NUMERALS[100].subtrahend = NUMERALS[10]
NUMERALS[500].subtrahend = NUMERALS[100]
NUMERALS[1000].subtrahend = NUMERALS[100]
end
def convert(number)
return NUMERALS[number].roman if NUMERALS[number]
to_roman(number)
@result
end
private
def to_roman(number)
NUMERALS.each do |value, numeral|
number = roman_reduce(number, numeral)
number = roman_reduce(number, numeral.difference) if numeral.difference
end
end
def roman_reduce(number, numeral)
while number >= numeral.arabic
@result += numeral.roman
number -= numeral.arabic
end
number
end
end
Now it is obvious that we are subtracting the subtrahend from the minuend in order to ascertain the difference. Right?
Right?
Damn. Still doesn't "feel" right.
I'm trying it again. Stay tuned.