Doing Math with Roman Numbers


You want to convert between Arabic and Roman numbers, or do arithmetic with Roman numbers and get Roman numbers as your result.


The simplest way to define a Roman class that acts like Fixnum is to have its instances delegate most of their method calls to a real Fixnum (as seen in the previous recipe, Recipe 2.13). First we'll implement a container for the Fixnum delegate, and methods to convert between Roman and Arabic numbers:

	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 not in
	 # 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)
	 @value = number

	 # Raise an exception if a number is too large or small to be represented
	 # as a 
Roman number.
	 unless number.between?(1, MAX)
	 msg = "#{number} can't be represented as a Roman number."

	 #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 ="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
	 c = c[roman.size..s.size]

	 #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 >=
	 raise invalid

	 value += value_from_this_number
	 value_from_previous_number = value_from_this_number
	 value_of_previous_number = arabic
	 break if c.size == 0
	 raise invalid if c.size > 0
	 return value

	 def to_arabic
	 #Render a Fixnum as a string depiction of a 
Roman number
	 def to_ 
	 value = to_arabic
	 repr = ""
	 @@arabic_to_roman.reverse_each do |arabic, roman|
	 num, value = value.divmod(arabic)
	 repr << roman * num

Next, we'll make the class respond to all of Fixnum's methods by implementing a method_missing that delegates to our internal Fixnum object. This is substantially the same method_missing as in Recipe 2.13 Whenever possible, we'll transform the results of a delegated method into Roman objects, so that operations on Roman objects will yield other Roman objects.

	# 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
	 result = @value.send(m, *hex_args)
	 return result if m == :coerce
	 case result
	 when Integer
	 when Array
	 result.collect do |element|
	 element.kind_of?(Integer) ? : element
	 rescue RangeError
	 # Too big or small to fit in a Roman number. Use the original number

The only methods that won't trigger method_missing are methods like to_s, which we're going to override with our own implementations:

	 def respond_to?(method_name)
	 super or @value.respond_to? method_name

	 def to_s

	 def inspect

We'll also add methods to Fixnum and String that make it easy to create Roman objects:

	class Fixnum
	 def to_roman

	class String
	 def to_roman

Now we're ready to put the Roman class through its paces:

	72.to_roman # => LXXII
	444.to_roman # => CDXLIV
	1979.to_roman # => MCMLXXIX
	'MCMXLVIII'.to_roman # => MCMXLVIII

	Roman.to_arabic('MCMLXXIX') # => 1979
	'MMI'.to_roman.to_arabic # => 2001

	'MMI'.to_roman + 3 # => MMIV
	'MCMXLVIII'.to_roman # => MCMXLVIII
	612.to_roman * 3.to_roman # => MDCCCXXXVI
	(612.to_roman * 3).divmod('VII'.to_roman) # => [CCLXII, II]
	612.to_roman * 10000 # => 6120000 # Too big
	612.to_roman * 0 # => 0 # Too small

	'MCMXCIX'.to_roman.succ # => MM

	# => [I, II, III, IV, V, VI, VII, VIII, IX, X]

Here are some invalid Roman numbers that the Roman class rejects:

	# ArgumentError: Invalid Roman number: IIII
	# ArgumentError: Invalid Roman number: IVI
	# ArgumentError: Invalid Roman number: IXV
	# ArgumentError: Invalid Roman number: MCMM
	# ArgumentError: Invalid 
Roman number: CIVVM
	# RangeError: -10 can't be represented as a Roman number.
	# RangeError: 50000 can't be represented as a Roman number.



The rules for constructing Roman numbers are more complex than those for constructing positional numbers such as the Arabic numbers we use. An algorithm for parsing an Arabic number can scan from the left, looking at each character in isolation. If you were to scan a Roman number from the left one character at a time, you'd often find yourself having to backtrack, because what you thought was "XI" (11) would frequently turn out to be "XIV" (14).

The simplest way to parse a Roman number is to adapt the algorithm so that (for instance) "IV" as treated as its own "character," distinct from "I" and "V". If you have a list of all these "characters" and their Arabic values, you can scan a Roman number from left to right with a greedy algorithm that keeps a running total. Since there are few of these "characters" (only 13 of them, for numbers up to 3,999), and none of them are longer than 2 letters, this algorithm is workable. To generate a Roman number from an Arabic number, you can reverse the process.

The Roman class given in the Solution works like Fixnum, thanks to the method_missing strategy first explained in Recipe 2.13. This lets you do math entirely in Roman numbers, except when a result is out of the supported range of the Roman class.

Since this Roman implementation only supports 3999 distinct numbers, you could make the implementation more efficient by pregenerating all of them and retrieving them from a cache as needed. The given implementation lets you extend the implementation to handle larger numbers: you just need to decide on a representation for the larger Roman characters that will work for your encoding.

The Roman numeral for 5,000 (a V with a bar over it) isn't present in ASCII, but there are Unicode characters U+2181 (the Roman numeral 5,000) and U+2182 (the Roman numeral 10,000), so that's the obvious choice for representing Roman numbers up to 39,999. If you're outputting to HTML, you can use a CSS style to put a bar above "V", "X", and so on. If you're stuck with ASCII, you might choose "_V" to represent 5,000, "_X" to represent 10,000, and so on. Whatever you chose, you'd add the appropriate "characters" to the roman_to_arabic array (remembering to add "M_V" and "_V_X" as well as "_V" and "_X"), increment MAX, and suddenly be able to instantiate Roman objects for large numbers.

The Roman#to_arabic method implements the "new" rules for Roman numbers: that is, the ones standardized in the Middle Ages. It rejects certain number representations, like IIII, used by the Romans themselves.

Roman numbers are common as toy or contest problems, but it's rare that a programmer will have to treat a Roman number as a number, as opposed to a funny-looking string. In parts of Europe, centuries and the month section of dates are written using Roman numbers. Apart from that, outline generation is probably the only real-world application where a programmer needs to treat a Roman number as a number. Outlines need several of visually distinct ways to represent the counting numbers, and Roman numbers (upper- and lowercase) provide two of them.

If you're generating an outline in plain text, you can use Roman#succ to generate a succession of Roman numbers. If your outline is in HTML format, though, you don't need to know anything about Roman numbers at all. Just give an

  1. tag a CSS style of list-style-type:lower-roman or list-style-type:upper-roman. Output the elements of your outline as
  2. tags inside the
    1. tag. All modern browsers will do the right thing:
      1. Primus
      2. Secundis
      3. Tertius

      See Also

      • Recipe 2.13, "Simulating a Subclass of Fixnum"
      • An episode of the Ruby Quiz focused on algorithms for converting between Roman and Arabic numbers; one solution uses an elegant technique to make it easier to create Roman numbers from within Ruby: it overrides Object#const_ missing to convert any undefined constant into a Roman number; this lets you issue a statement like XI + IX, and get XX as the result (



Date and Time



Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming


Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration

Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399 © 2008-2020.
If you may any questions please contact us: