Version 1.3
[lematema] / roman_numerals.rb
1 #!/usr/bin/ruby\r
2 #\r
3 # Roman numerals\r
4 #\r
5 # Generates roman numerals from integers and vice-versa\r
6 #\r
7 # A response to Ruby Quiz of the Week #22 - Roman Numerals [ruby-talk:132925]\r
8 #\r
9 # Author: Dave Burt <dave at burt.id.au>\r
10 #\r
11 # Created: 7 Mar 2005\r
12 #\r
13 # Last modified: 8 Mar 2005\r
14 #\r
15 # Fine print: Provided as is. Use at your own risk. Unauthorized copying is\r
16 #             not disallowed. Credit's appreciated if you use my code. I'd\r
17 #             appreciate seeing any modifications you make to it.\r
18 #\r
19 \r
20 # Contains methods to convert integers to roman numeral strings and vice-versa.\r
21 module RomanNumerals\r
22   \r
23   # Maps roman numeral digits to their integer values\r
24   DIGITS = {\r
25     'I' => 1,\r
26     'V' => 5,\r
27     'X' => 10,\r
28     'L' => 50,\r
29     'C' => 100,\r
30     'D' => 500,\r
31     'M' => 1000,\r
32   }\r
33   \r
34   # The largest integer representable as a roman numerable by this module\r
35   MAX = 3999\r
36   \r
37   # Maps some integers to their roman numeral values\r
38   @@digits_lookup = DIGITS.inject({\r
39     4 => 'IV',\r
40     9 => 'IX',\r
41     40 => 'XL',\r
42     90 => 'XC',\r
43     400 => 'CD',\r
44     900 => 'CM',}) do |memo, pair|\r
45     memo.update({pair.last => pair.first})\r
46   end\r
47   \r
48   # Stolen from O'Reilly's Perl Cookbook 6.23. Regular Expression Grabbag\r
49   REGEXP = /^M*(D?C{0,3}|C[DM])(L?X{0,3}|X[LC])(V?I{0,3}|I[VX])$/i\r
50   \r
51   # Converts +int+ to a roman numeral\r
52   def self.from_integer(int)\r
53     return nil if int < 0 || int > MAX\r
54     remainder = int\r
55     result = ''\r
56     @@digits_lookup.keys.sort.reverse.each do |digit_value|\r
57       while remainder >= digit_value\r
58         remainder -= digit_value\r
59         result += @@digits_lookup[digit_value]\r
60       end\r
61       break if remainder <= 0\r
62     end\r
63     result\r
64   end\r
65   \r
66   # Converts +roman_string+, a roman numeral, to an integer\r
67   def self.to_integer(roman_string)\r
68     return nil unless roman_string.is_roman_numeral?\r
69     last = nil\r
70     roman_string.to_s.upcase.split(//).reverse.inject(0) do |memo, digit|\r
71       if digit_value = DIGITS[digit]\r
72         if last && last > digit_value\r
73           memo -= digit_value\r
74         else\r
75           memo += digit_value\r
76           end\r
77         last = digit_value\r
78       end\r
79       memo\r
80     end\r
81   end\r
82   \r
83   # Returns true iif +string+ is a roman numeral.\r
84   def self.is_roman_numeral?(string)\r
85     REGEXP =~ string\r
86   end\r
87 end\r
88 \r
89 class String\r
90   # Considers string a roman numeral numeral,\r
91   # and converts it to the corresponding integer.\r
92   def to_i_roman\r
93     RomanNumerals.to_integer(self)\r
94   end\r
95   # Returns true iif the subject is a roman numeral.\r
96   def is_roman_numeral?\r
97     RomanNumerals.is_roman_numeral?(self)\r
98   end\r
99 end\r
100 class Integer\r
101   # Converts this integer to a roman numeral.\r
102   def to_s_roman\r
103     RomanNumerals.from_integer(self) || ''\r
104   end\r
105 end\r
106 \r
107 \r
108 # Integers that look like roman numerals\r
109 class RomanNumeral\r
110         attr_reader :to_s, :to_i\r
111 \r
112         @@all_roman_numerals = []\r
113 \r
114         # May be initialized with either a string or an integer\r
115         def initialize(value)\r
116                 case value\r
117                 when Integer\r
118                         @to_s = value.to_s_roman\r
119                         @to_i = value\r
120                 else\r
121                         @to_s = value.to_s\r
122                         @to_i = value.to_s.to_i_roman\r
123                 end\r
124                 @@all_roman_numerals[to_i] = self\r
125         end\r
126 \r
127         # Factory method: returns an equivalent existing object if such exists,\r
128         # or a new one\r
129         def self.get(value)\r
130                 if value.is_a?(Integer)\r
131                         to_i = value\r
132                 else\r
133                         to_i = value.to_s.to_i_roman\r
134                 end\r
135                 @@all_roman_numerals[to_i] || RomanNumeral.new(to_i)\r
136         end\r
137 \r
138         def inspect\r
139                 to_s\r
140         end\r
141 \r
142         # Delegates missing methods to Integer, converting arguments to Integer,\r
143         # and converting results back to RomanNumeral\r
144         def method_missing(sym, *args)\r
145                 unless to_i.respond_to?(sym)\r
146                         raise NoMethodError.new(\r
147                                 "undefined method '#{sym}' for #{self}:#{self.class}")\r
148                 end\r
149                 result = to_i.send(sym,\r
150                         *args.map {|arg| arg.is_a?(RomanNumeral) ? arg.to_i : arg })\r
151                 case result\r
152                 when Integer\r
153                         RomanNumeral.get(result)\r
154                 when Enumerable\r
155                         result.map do |element|\r
156                                 element.is_a?(Integer) ? RomanNumeral.get(element) : element\r
157                         end\r
158                 else\r
159                         result\r
160                 end\r
161         end\r
162 end\r
163 \r
164 # Enables uppercase roman numerals to be used interchangeably with integers.\r
165 # They are auto-vivified RomanNumeral constants\r
166 # Synopsis:\r
167 #   4 + IV           #=> VIII\r
168 #   VIII + 7         #=> XV\r
169 #   III ** III       #=> XXVII\r
170 #   VIII.divmod(III) #=> [II, II]\r
171 def Object.const_missing sym\r
172         raise NameError.new("uninitialized constant: #{sym}") unless RomanNumerals::REGEXP === sym.to_s\r
173         const_set(sym, RomanNumeral.get(sym))\r
174 end\r
175 \r
176 \r
177 # Quiz solution: filter that swaps roman and arabic numbers\r
178 if __FILE__ == $0\r
179   ARGF.each do |line|\r
180     line.chomp!\r
181     if line.is_roman_numeral?\r
182       puts line.to_i_roman\r
183     else\r
184       puts line.to_i.to_s_roman\r
185     end\r
186   end\r
187 end\r