Skip to content

Latest commit

 

History

History
449 lines (368 loc) · 13.1 KB

roman_numerals.md

File metadata and controls

449 lines (368 loc) · 13.1 KB

罗马数字

这次的题目是要你写一个普通数字和罗马数字的转换器。

这个脚本应该是个标准Unix过滤器,从命令行指定的文件或者STDIN读入并写到STDOUT。每行输入包括一个用阿拉伯或者罗马数字表示的整数(从1到3,999)。对应每行输入应该有一行输出,内容是原始数的另一种表达方式。

举例来说,对应下面的输入:

III
29
38
CCXCI
1999

正确的输出应该是这样:

3
XXIX
XXXVIII
291
MCMXCIX

如果你对罗马数字不太熟悉或者需要复习一下,很简单。首先,七个字母对应于七个值:

I = 1 
V = 5
X = 10
L = 50
C = 100 
D = 500 
M = 1000

其次,你可以将字母由大到小从左到右组合在一起来表示相加的值:

II    is 2
VIII  is 8
XXXI  is 31

然而,你最多只能将同一个字母连续使用三次。这样的话就需要一条特殊规则来表示像40和900这样的数。规则就是在一个较大的值前面加上一个较小的值来表示减少多少。这条规则用来表示先前的规则不能表示的数。像这些:

IV is 4
IX is 9
XL is 40
XC is 90
CD is 400
CM is 900

解答部分

解决这个问题很简单,到底多简单呢?这样说吧,问题已经给我们了一个转换表,可以轻松表示成一个Hash

roman_numerals/simple.rb

ROMAN_MAP = { 1     => "I",
              4     => "IV",
              5     => "V",
              9     => "IX",
              10    => "X",
              40    => "XL", 
              50    => "L", 
              90    => "XC", 
              100   => "C", 
              400   => "CD", 
              500   => "D", 
              900   => "CM", 
              1000  => "M" }

这就是我的代码,而大部分的解法都差不多如此。

这样的话我们就只需要to_roman()to_arabic()方法了,对吧?对我这样的懒骨头听起来有好多工作吖,所以我还是作弊好了。如果你有了一个转换表,那么你就能单向转换了。

roman_numerals/simple.rb

ROMAN_NUMERALS = Array.new(3999) do |index|
  target = index + 1
  ROMAN_MAP.keys.sort { |a, b| b <=> a }.inject("") do |roman, div|
    times, target = target.divmod(div)
    roman << ROMAN_MAP[div] * times
  end
end

这就是在许多解法中都差不多的to_roman()方法。我的办法就是填充一个数组。算法本身没啥难度。用每个罗马数字去除阿拉伯数得到的商,重复该罗马数字这么多次。Ruby的divmod()方法正好适合这个工作。

现在只需要加一层Unix接口了。但我还是喜欢先验证一下输入,于是就多写了一点正则工具:

roman_numerals/simple.rb

IS_ROMAN = / ^ M{0,3}
               (?:CM|DC{0,3}|CD|C{0,3})
               (?:XC|LX{0,3}|XL|X{0,3})
               (?:IX|VI{0,3}|IV|I{0,3}) $ /ix
IS_ARABIC = /^(?:[123]\d{3}|[1-9]\d{0,2})$/

第一个正则是验证罗马数字的,以十的幂分开。第二个正则匹配1..3999,这是我们能够转换的区间。

现在,可以写Unix接口了:

roman_numerals/simple.rb

if __FILE__ == $0
  ARGF.each_line() do |line|
    line.chomp!
    case line
    when IS_ROMAN  then puts ROMAN_NUMERALS.index(line) + 1
    when IS_ARABIC then puts ROMAN_NUMERALS[line.to_i - 1]
    else raise "Invalid input:  #{line}"
    end
  end
end

用普通话来表述就是,对于每行输入,如果能匹配IS_ROMAN,就在Array里查找。如果不能匹配IS_ROMAN而能匹配IS_ARABI,就在Array中直接索引该值。如果两项都不匹配,就报告输入错误。

####省点内存####

如果你不想遍历整个数组,只需要创建另外一套转换器,这也不难。J E Bailey的代码就做到了,我们来看看:

roman_numerals/dual_conversions.rb

@data = [
["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]
]

@roman = %r{^[CDILMVX]*$}
@arabic = %r{^[0-9]*$}

def to_roman(num)
  reply = ""
  for key, value in @data
    count, num = num.divmod(value)
    reply << (key * count)
  end
  reply
end

def to_arabic(rom)
  reply = 0
  for key, value in @data
    while rom.index(key) == 0
      reply += value
      rom.slice!(key)
    end
  end
  reply
end

$stdin.each do |line|
  case line
  when @roman
    puts to_arabic(line)
  when @arabic
    puts to_roman(line.to_i)
  end
end

告诉过你我们总会用到类似我的Hash的东西。这里就是个一对一对的数组。

这下面可以看到J E验证数据的正则表达式。和我的不太一样,但显然更清楚明了。

接下来是to_roman()方法,看起来很眼熟。这个实现和我的几乎一模一样,但由于没有引入数组而更清楚一点。

再下来是个有趣的部分,to_arabic()。该方法上来就将一个叫reply的变量赋值为0。然后依次遍历rom字符串中的每个罗马数字,将reply增加对应值,并将该数字从字符串中移除。@data数组的顺序保证了XL或者IV将在X或者I之前处理。

最后,代码给出了quiz指定的Unix接口。这又和我的解法很像了,也有两种转换方式分别进行。

罗马ruby

上面都是小儿科,我们还是看看Dave Burt的Ruby魔术。Dave的代码包含了一个模块,RomanNumerals,以及和我们之前看过的很相似的to_integer()from_integer()。该模块还定义了is_roman_numeral?()方法来检查输入,另外还有些有用的常量,如DIGITSMAXREGEXP

roman_numerals/roman_numerals.rb

# Contains methods to convert integers to Roman numeral strings, and vice versa.
module RomanNumerals
  
  # Maps Roman numeral digits to their integer values
  DIGITS = {
    'I' => 1,
    'V' => 5,
    'X' => 10,
    'L' => 50,
    'C' => 100,
    'D' => 500,
    'M' => 1000
  }
  
  # The largest integer representable as a Roman numerable by this module
  MAX = 3999
  
  # Maps some integers to their Roman numeral values
  @@digits_lookup = DIGITS.inject({
    4 => 'IV',
    9 => 'IX',
    40 => 'XL',
    90 => 'XC',
    400 => 'CD',
    900 => 'CM'}) do |memo, pair|
    memo.update({pair.last => pair.first})
  end
  
  # Based on Regular Expression Grabbag in the O'Reilly Perl Cookbook, #6.23
  REGEXP = /^M*(D?C{0,3}|C[DM])(L?X{0,3}|X[LC])(V?I{0,3}|I[VX])$/i
  
  # Converts +int+ to a Roman numeral
  def self.from_integer(int)
    return nil if int < 0 || int > MAX
    remainder = int
    result = ''
    @@digits_lookup.keys.sort.reverse.each do |digit_value|
      while remainder >= digit_value
        remainder -= digit_value
        result += @@digits_lookup[digit_value]
      end
      break if remainder <= 0
    end
    result
  end
  
  # Converts +roman_string+, a Roman numeral, to an integer
  def self.to_integer(roman_string)
    return nil unless roman_string.is_roman_numeral?
    last = nil
    roman_string.to_s.upcase.split(//).reverse.inject(0) do |memo, digit|
      if digit_value = DIGITS[digit]
        if last && last > digit_value
          memo -= digit_value
        else
          memo += digit_value
        end
        last = digit_value
      end
      memo
    end
  end
  
  # Returns true if +string+ is a Roman numeral.
  def self.is_roman_numeral?(string)
    REGEXP =~ string
  end
end

我觉得我们应该再过一下这段代码,但我想先指出一个闪光点。 注意看Dave是如何以一种优雅的方式将IV这样的数从DIGITS中剥离的。其中用了个不寻常的写法memo.update({pair.last=>pair.first}),而非看起来更常见的memo[pair.last]=pair.first。是因为前者返回Hash本身,而inject()循环是需要不断更新这个变量的。

好了,这个模块只是Dave代码的一部分,剩下的也很有意思。来看看他是如何使用该模块的:

roman_numerals/roman_numerals.rb

class String
  # Considers string a Roman numeral,
  # and converts it to the corresponding integer.
  def to_i_roman
    RomanNumerals.to_integer(self)
  end
  # Returns true if the subject is a Roman numeral.
  def is_roman_numeral?
    RomanNumerals.is_roman_numeral?(self)
  end
end
class Integer
  # Converts this integer to a Roman numeral.
  def to_s_roman
    RomanNumerals.from_integer(self) || ''
  end
end

首先,他给StringInteger类添加了转换方法。然后你就能编写像如下的代码了:

puts "In the year #{1999.to_s_roman} ..."

有意思吧,更好玩的在后面。Dave的究极奥义是定义了这样一个类:

roman_numerals/roman_numerals.rb

# Integers that look like Roman numerals
class RomanNumeral
  attr_reader :to_s, :to_i

  @@all_roman_numerals = []

  # May be initialized with either a string or an integer
  def initialize(value)
    case value
    when Integer
      @to_s = value.to_s_roman
      @to_i = value
    else
      @to_s = value.to_s
      @to_i = value.to_s.to_i_roman
    end
    @@all_roman_numerals[to_i] = self
  end

  # Factory method: returns an equivalent existing object if such exists,
  # or a new one
  def self.get(value)
    if value.is_a?(Integer)
      to_i = value
    else
      to_i = value.to_s.to_i_roman
    end
    @@all_roman_numerals[to_i] || RomanNumeral.new(to_i)
  end

  def inspect
    to_s
  end

  # Delegates missing methods to Integer, converting arguments to Integer,
  # and converting results back to RomanNumeral
  def method_missing(sym, *args)
    unless to_i.respond_to?(sym)
      raise NoMethodError.new(
        "undefined method '#{sym}' for #{self}:#{self.class}")
    end
    result = to_i.send(sym,
      *args.map {|arg| arg.is_a?(RomanNumeral) ? arg.to_i : arg })
    case result
    when Integer
      RomanNumeral.get(result)
    when Enumerable
      result.map do |element|
        element.is_a?(Integer) ? RomanNumeral.get(element) : element
      end
    else
      result
    end
  end
end



# Enables uppercase Roman numerals to be used interchangeably with integers.
# They are autovivified RomanNumeral constants
# Synopsis:
#   4 + IV           #=> VIII
#   VIII + 7         #=> XV
#   III ** III       #=> XXVII
#   VIII.divmod(III) #=> [II, II]
def Object.const_missing sym
  unless RomanNumerals::REGEXP === sym.to_s
    raise NameError.new("uninitialized constant: #{sym}")
  end
  const_set(sym, RomanNumeral.get(sym))
end



# Quiz solution: filter that swaps Roman and arabic numbers
if __FILE__ == $0
  ARGF.each do |line|
    line.chomp!
    if line.is_roman_numeral?
      puts line.to_i_roman
    else
      puts line.to_i.to_s_roman
    end
  end
end

用工厂方法get()来创建对象会很高效且可复用,它对于同样的数值总会给出同一个对象。

注意method_missing()方法最终会把处理委托给Integer类,所以你可以将这些对象当做Integer实例来看。这个类能让你写出如下的代码:

IV = RomanNumeral.get(4)
IV + 5 # => IX

更神奇的是,多亏Dave,连第一步的代码都不需要了:

roman_numerals/roman_numerals.rb

# Enables uppercase Roman numerals to be used interchangeably with integers.
# They are autovivified RomanNumeral constants
# Synopsis:
#   4 + IV           #=> VIII
#   VIII + 7         #=> XV
#   III ** III       #=> XXVII
#   VIII.divmod(III) #=> [II, II]
def Object.const_missing sym
  unless RomanNumerals::REGEXP === sym.to_s
    raise NameError.new("uninitialized constant: #{sym}")
  end
  const_set(sym, RomanNumeral.get(sym))
end

这样的话Ruby在必要时就会自动将IX这样的常量当做RomanNumeral对象来看待了,太爽了。

最后,(原书)下一页顶端是Dave应用上面这些工具对这个问题的解决过程。

roman_numerals/roman_numerals.rb

# Quiz solution: filter that swaps Roman and arabic numbers
if __FILE__ == $0
  ARGF.each do |line|
    line.chomp!
    if line.is_roman_numeral?
      puts line.to_i_roman
    else
      puts line.to_i.to_s_roman
    end
  end
end

####补充练习#### 1.修改你的代码,使之能扫描任何文本文档,将其中所有的合法罗马数替换成对应的阿拉伯数。

2.重新写一个类似于第一种解法的方案,但不能在内存中维护一个有4000个元素的数组。