 
 This page contains automated test results for code from O'Reilly's Ruby Cookbook. If this code looks interesting or useful, you might want to buy the whole book.
| Doing Math with Roman Numbers | ||
|---|---|---|
| Code | Expected | Actual | 
| class Roman
  # These arrays map all distinct substrings of Roman numbers
  # to their Arabic equivalents, and vice versa.
  @@roman_to_arabic = [['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]]
  @@arabic_to_roman = @@roman_to_arabic.collect { |x| x.reverse }.reverse
  # The Roman symbol for 5000 (a V with a bar over it) is in neither
  # ASCII nor Unicode, so we won't represent numbers larger than 3999.
  MAX = 3999
  def initialize(number)
    if number.respond_to? :to_str
      @value = Roman.to_arabic(number)
    else
      Roman.assert_within_range(number)
      @value = number
    end
  end  
  # Raise an exception if a number is too large or small to be represented
  # as a Roman number.
  def Roman.assert_within_range(number)
    unless number.between?(1, MAX)
      msg = "#{number} can't be represented as a Roman number."
      raise RangeError.new(msg)
    end
  end
  #Find the Fixnum value of a string containing a Roman number.
  def Roman.to_arabic(s)
    value = s
    if s.respond_to? :to_str
      c = s.dup
      value = 0
      invalid = ArgumentError.new("Invalid Roman number: #{s}")
      value_of_previous_number = MAX+1
      value_from_previous_number = 0
      @@roman_to_arabic.each_with_index do |(roman, arabic), i|
        value_from_this_number = 0
        while c.index(roman) == 0
          value_from_this_number += arabic
          if value_from_this_number >= value_of_previous_number
            raise invalid
          end
          c = c[roman.size..s.size]
        end
        #This one's a little tricky. We reject numbers like "IVI" and
        #"IXV", because they use the subtractive notation and then
        #tack on a number that makes the total overshoot the number
        #they'd have gotten without using the subtractive
        #notation. Those numbers should be V and XIV, respectively.
        if i > 2 and @@roman_to_arabic[i-1][0].size > 1 and 
            value_from_this_number + value_from_previous_number >= 
            @@roman_to_arabic[i-2][1]
          raise invalid
        end
        value += value_from_this_number
        value_from_previous_number = value_from_this_number
        value_of_previous_number = arabic
        break if c.size == 0
      end
      raise invalid if c.size > 0
    end
    return value
  end
  def to_arabic
    @value
  end
  #Render a Fixnum as a string depiction of a Roman number
  def to_roman
    value = to_arabic
    Roman.assert_within_range(value)
    repr = ""
    @@arabic_to_roman.reverse_each do |arabic, roman|
      num, value = value.divmod(arabic)
      puts "Roman #{roman} num #{num}"
      repr << roman * num
    end
    repr
  end  
  # Delegate all methods to the stored integer value. If the result is
  # a Integer, transform it into a Roman object. If it's an array
  # containing Integers, transform it into an array containing Roman
  # objects.
  def method_missing(m, *args)
    super unless @value.respond_to?(m)
    hex_args = args.collect do |arg| 
      arg.kind_of?(Roman) ? arg.to_int : arg 
    end
    result = @value.send(m, *hex_args)
    return result if m == :coerce
    begin
      case result     
      when Integer
        Roman.new(result)
      when Array
        result.collect do |element|
          element.kind_of?(Integer) ? Roman.new(element) : element
        end
      else
        result
      end
    rescue RangeError
      # Too big or small to fit in a Roman number. Use the Fixnum.
      result
    end
  end
  def respond_to?(method_name) 
    super or @value.respond_to? method_name 
  end
  def to_s
    to_roman
  end
  def inspect
    to_s
  end
end
class Fixnum
  def to_roman
    Roman.new(self)
  end
end
class String
  def to_roman
    Roman.new(self)
  end
end
72.to_roman | LXXII | LXXII | 
| 444.to_roman | CDXLIV | CDXLIV | 
| 1979.to_roman | MCMLXXIX | MCMLXXIX | 
| 'MCMXLVIII'.to_roman | MCMXLVIII | MCMXLVIII | 
| Roman.to_arabic('MCMLXXIX') | 1979 | 1979 | 
| 'MMI'.to_roman.to_arabic | 2001 | 2001 | 
| 'MMI'.to_roman + 3 | MMIV | MMIV | 
| 'MCMXLVIII'.to_roman | MCMXLVIII | MCMXLVIII | 
| 612.to_roman * 3.to_roman | MDCCCXXXVI | MDCCCXXXVI | 
| (612.to_roman * 3).divmod('VII'.to_roman) | [CCLXII, II] | [CCLXII, II] | 
| 612.to_roman * 10000 | 6120000 | 6120000 | 
| 612.to_roman * 0 | 0 | 0 | 
| 'MCMXCIX'.to_roman.succ | MM | MM | 
| ('I'.to_roman..'X'.to_roman).collect | [I, II, III, IV, V, VI, VII, VIII, IX, X] | [I, II, III, IV, V, VI, VII, VIII, IX, X] | 
| 'IIII'.to_roman | ArgumentError: Invalid Roman number: IIII ... | ArgumentError: Invalid Roman number: IIII from (irb):41:in `to_arabic' from (irb):140:in `each_with_index' from (irb):36:in `to_arabic' from (irb):13:in `initialize' from (irb):123:in `to_roman' from (irb):140 from :0 | 
| 'IVI'.to_roman | ArgumentError: Invalid Roman number: IVI ... | ArgumentError: Invalid Roman number: IVI from (irb):53:in `to_arabic' from (irb):141:in `each_with_index' from (irb):36:in `to_arabic' from (irb):13:in `initialize' from (irb):123:in `to_roman' from (irb):141 from :0 | 
| 'IXV'.to_roman | ArgumentError: Invalid Roman number: IXV ... | ArgumentError: Invalid Roman number: IXV from (irb):53:in `to_arabic' from (irb):142:in `each_with_index' from (irb):36:in `to_arabic' from (irb):13:in `initialize' from (irb):123:in `to_roman' from (irb):142 from :0 | 
| 'MCMM'.to_roman | ArgumentError: Invalid Roman number: MCMM ... | ArgumentError: Invalid Roman number: MCMM from (irb):60:in `to_arabic' from (irb):13:in `initialize' from (irb):123:in `to_roman' from (irb):143 from :0 | 
| 'CIVVM'.to_roman | ArgumentError: Invalid Roman number: CIVVM ... | ArgumentError: Invalid Roman number: CIVVM from (irb):60:in `to_arabic' from (irb):13:in `initialize' from (irb):123:in `to_roman' from (irb):144 from :0 | 
| -10.to_roman | RangeError: -10 can't be represented as a Roman number. ... | RangeError: -10 can't be represented as a Roman number. from (irb):24:in `assert_within_range' from (irb):15:in `initialize' from (irb):118:in `to_roman' from (irb):145 from :0 | 
| 50000.to_roman | RangeError: 50000 can't be represented as a Roman number. ... | RangeError: 50000 can't be represented as a Roman number. from (irb):24:in `assert_within_range' from (irb):15:in `initialize' from (irb):118:in `to_roman' from (irb):146 from :0 |