Roman Numeral Kata

While working on the Roman Numeral Kata, I was bothered by the existence of special pairs in my numerals hash.
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.

1 comment:

  1. Hey Doc:

    Excellent and valiant: trying to get the code to represent the problem domain. And, of course, you lost me at subtrahend. ;-)

    Actually, in all honesty, what I want is something other than complex-dataset-mapping altogether. I want domain operands (numerals) and operators to represent the underlying arithmetic, as well as the mental operation I do when I look at "IX"

    If you could use method chaining sorts of methods, what kinds of operands might emerge on the Numeral class? Including constructors?

    Can I have I.X.roman = "IX"
    and I.X.arabic = 9 ?

    Please?

    --Patrick

    ReplyDelete