Ruby - method_missing

3
我将尝试实现一个 method_missing 方法,用于将美元转换为其他货币,例如 5 美元应该得到 5,5 日元应该得到 0.065,5 欧元应该得到 6.56 等等。我现在已经做到了这一点。现在我需要实现当执行 5.dollars.in(:yen) 等命令时能够正常工作。
目前我的代码如下:
class Numeric
  @@currencies = {'yen' => 0.013, 'euro' => 1.292, 'rupee' => 0.019}
  def method_missing(method_id)
    singular_currency = method_id.to_s.gsub( /s$/, '')
    if @@currencies.has_key?(singular_currency)
      self * @@currencies[singular_currency]
    else
      super
    end
  end
end

有人能解释一下我该怎么做吗?

附注:我宁愿你不给我代码,而是给我解释,这样我可以自己确定如何完成。


1
我正在解决同样的问题,有趣的是这篇帖子现在已经成为谷歌搜索“ruby method_missing”前十名的文章之一。 - cori
9个回答

10

添加了货币 'dollar' 并在方法中使用:

class Numeric
  @@currencies = {'dollar' => 1, 'yen' => 0.013, 'euro' => 1.292, 'rupee' => 0.019}
  def method_missing(method_id)
    singular_currency = method_id.to_s.gsub(/s$/, '')
    if @@currencies.has_key?(singular_currency)
      self * @@currencies[singular_currency]
    else
      super
    end
  end

  def in(currency)
    singular_currency = currency.to_s.gsub(/s$/, '')
    self / @@currencies[singular_currency]
  end
end

5
也许这个例子会更有帮助。这是一个可行的示例(请注意,我期望您已经安装了Rails的一部分ActiveSupport和Ruby 1.9.2+):
require 'rubygems'

# This is allowing us to do the `pluralize` calls below
require 'active_support/inflector'

module Currency
  CONVERSION_TABLE = { dollars: { dollars: 1, euros: 0.75 }, euros: { dollars: 1.3333334, euros: 1 } }.freeze
  attr_accessor :currency

  def method_missing(method_name, *args, &block)
    # standardize on pluralized currency names internally so both singular
    # and plural methods are handled
    method_name = method_name.to_s.pluralize.to_sym

    # Use the "from" keys in the conversion table to verify this is a valid 
    # source currency
    if CONVERSION_TABLE.key?(method_name)
      @currency = method_name
      self # return self so a call to `1.dollar` returns `1` and not `:dollars`
    else
      super
    end
  end

  # Convert `self` from type of `@currency` to type of `destination_currency`, mark the result with
  # the appropriate currency type, and return. Example:
  def to(destination_currency)
    # Again, standardize on plural currency names internally
    destination_currency = destination_currency.to_s.pluralize.to_sym

    # Do some sanity checking
    raise UnspecifiedSourceCurrency unless defined?(@currency)
    raise UnsupportedDestinationCurrency unless CONVERSION_TABLE.key?(destination_currency)

    # Do the actual conversion, and round for sanity, though a better
    # option would be to use BigDecimal which is more suited to handling money
    result = (self * CONVERSION_TABLE[@currency][destination_currency]).round(2)

    # note that this is setting @currency through the accessor that
    # was created by calling `attr_accessor :currency` above
    result.currency = destination_currency
    result
  end
end

class Numeric
  # Take all the functionality from Currency and mix it into Numeric
  # 
  # Normally this would help us encapsulate, but right now it's just making
  # for cleaner reading. My original example contained more encapsulation
  # that avoided littering the Numeric clas, but it's harder for a beginner
  # to understand. For now, just start here and you will learn more later.
  include Currency
end

p 5.euros.to(:dollars)                #=> 6.67
p 0.25.dollars.to(:euro)              #=> 0.19
p 1.dollar.to(:euros).to(:dollar)     #=> 1.0

3
这更像是一个数学问题而不是计算问题。
@@currencies哈希值中的每个值都被归一化为“美元”:它们的单位是日元/美元、欧元/美元和卢比/美元。对于5.euro.in(:yen),你只需要将欧元/美元除以日元/美元,就可以将答案表示为日元的欧元。
要使用Ruby计算这个问题,保持method_missing方法不变,并更新类常量以包括'dollar' => 1。添加一个Numeric#in方法,其中包含一行计算来解决此问题。该计算需要按照正确的顺序对浮点数进行除法运算。
对于5.euro.in(:yen)示例,请记住首先计算5.euro,但其单位为欧元/美元。接下来应用in(:yen)方法到该数字的倒数上。这将给出一个单位为日元/欧元的数字,即所需结果的倒数。

2
你是否可以定义一个叫做in的方法,并将符号参数发送回self
irb(main):057:0> 5.dollar.in(:euro)
=> 6.46
irb(main):065:0> 5.euro.in(:dollar)
=> 6.46 # Which is wrong, by the way

所以,不完全正确,因为你不知道当前金额代表什么意思 -- 你的 method_missing 假设所有金额都是以美元计算的。

这就是为什么有 money gem 存在的原因 :)


这是斯坦福SaaS课程的作业,所以我正在学习如何完成它,你能详细说明一下吗? - 8vius
@8vius 5.euro 返回一个数字,但返回值只是另一个FixNum; 你没有办法知道它代表什么货币。如果你假设每个数字都表示美元,那么我的想法就是将符号发送回给这个数字失败了,因为这意味着method_missing试图做两件事情。所以你可以做一些更接近其他答案建议的东西,in函数可以对符号进行简单的查找,以获取转换系数。 - Dave Newton

1

与其在此使用method_missing,更容易的方法是遍历每种货币并为它们定义单复数方法,将它们委托给您的转换方法。

出于方便起见,我假设您在这里使用ActiveSupport。没有这样的东西也可以做任何事情,但像constantize和concerns之类的东西会使它变得更容易。

module DavesMoney
  class BaseMoney
    # your implementation
  end

  class DollarConverter < BaseMoney
    def initialize(value)
      @value = value
    end

    def to(:currency)
      # implemented in `BaseMoney` that gets extended (or included)
    end
  end
end

module CurrencyExtension
  extend ActiveSupport::Concern

  SUPPORTED_CURRENCIES = %w{ dollar yen euro rupee }

  included do
    SUPPORTED_CURRENCIES.each do |currency|
      define_method :"#{currency}" do
        return "#{currency}_converter".constantize.new(self)
      end
      alias :"#{currency.pluralize}" :"#{currency}"
    end
  end
end

# extension
class Numeric
  include CurrencyExtension
end

2
我在这里展示的是更高效、自解释的元编程。这不是一个学习如何使用method_missing的好例子,除非你需要动态查找货币转换表。如果你想真正学习Ruby元编程,请获取Paolo Perrotta撰写的《Ruby元编程》一书。 - coreyward
当然不一定要使用method_missing,我该如何实现Numeric.dollars.in(:euro)呢? - 8vius
我希望你能从我的代码示例中推断出这一点,但是你需要记录初始货币以及目标货币才能进行转换。你可以存储一个查找表,但是Numeric不是一个合适的地方。理想情况下,这应该在你可以轻松更新它的地方发生...也许有一个服务可以每天连接并检索更新的值,我不知道。无论如何,你需要知道初始值,我认为这就是你提出API建议的原因所在。 - coreyward
希望你能理解,但我对Ruby和元编程都很陌生,你的例子基本上让我一头雾水。 - 8vius
如果你更喜欢使用method_missing来处理事情,你可以这样做,只需在我的示例中切换掉使用define_method。其背后的基本思想仍然相同:返回一个代理对象,在发送货币名称时知道初始值,然后使用to调用转换数值。在我看来,这仍然不是很好,因为最终你会得到另一个无单位的数字;一个更好的方法可能是一个模块,你可以将其包含到数值类型中,它简单地处理货币跟踪并等待调用convert来改变其值。 - coreyward
显示剩余4条评论

1

针对这个问题,我的方法是接受其限制(扩展Numeric上的method_missing实现,尽管正如@coreyward所指出的那样,这对于任何不是作业问题的东西来说都是错误的方法),具体做法如下:

理解5.euros.in(:yen)可以被翻译为:

eur = 5.send(:euros)
eur.send( :in, yen )

本质上发生的事情是我们将欧元信息发送到数字5,然后使用参数为:yen的Numeric 5.euros的结果发送in方法。

在method_missing中,您应该响应euros调用,并返回欧元转换为美元的结果,然后(也在method_missing中)响应in调用,并使用从先前调用中获得的美元转换为传递给in调用的符号的结果。这将返回正确的值。

当然,只要您的转换因子正确,您可以将货币转换为任何您想要的货币-对于这个特定问题的给定,将货币转换为/从美元似乎是最明智的选择。


1

我也正在学习这门课程,我看到了一些如何完成任务的示例。在某个时候提到了self.send,我相信其他人也实现了这个,但我发现这个解决方案适合我:

https://gist.github.com/2065412


-1

这是我做的...

http://pastebin.com/DpE8VAH4

    class Numeric
      @@currencies = {'yen' => 0.013, 'euro' => 1.292, 'rupee' => 0.019, 'dollar' => 1}
      def method_missing(method, *arg)
        singular_currency = method.to_s.gsub(/s$/,'')
        if @@currencies.has_key?(singular_currency)
          self * @@currencies[singular_currency]
        else
          super
        end
      end
      def in(arg)
        singular_currency = arg.to_s.gsub(/s$/,'')
        if @@currencies.has_key?(singular_currency)
          self * @@currencies[singular_currency]
        end
      end
    end
puts "5欧元 = "+5.euro.to_s puts "5欧元 = "+5.euros.to_s puts "5美元换算成欧元 = "+5.dollars.in(:euros).to_s puts "10欧元换算成卢比 = "+10.euros.in(:rupees).to_s
  • 在货币中添加 "'dollar' => 1"
  • 在 method_missing 方法中添加一个新的参数 ", *args"
  • 在 Numeric 类中添加一个新方法 "in(arg)"
  • 此方法将自身乘以由参数 "arg" 指定的货币

-1

首先,安装我的units库:gem install sy。然后,定义:

require 'sy'
Money = SY::Quantity.dimensionless      #=> #<Quantity:Money>
USD = SY::Unit.standard of: Money       #=> #<Unit:USD of Money >
YEN = SY::Unit.of Money, amount: 0.013  #=> #<Unit:YEN of Money >
EUR = SY::Unit.of Money, amount: 1.292  #=> #<Unit:EUR of Money >
INR = SY::Unit.of Money, amount: 0.019  #=> #<Unit:INR of Money >

现在你可以进行计算:

10 * 10.usd => #<Magnitude: 100 >
100.yen.in :usd #=> #<Magnitude: 1.3 >
1.eur + 1.usd #=> #<Magnitude: 2.29 >

你也可以定义

CENT = SY::Unit.of Money, amount: 0.01.usd
EUROCENT = SY::Unit.of Money, amount: 0.01.eur

然后

12.usd + 90.cent #=> #<Magnitude: 12.9 >

授人以鱼不如授人以渔... - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接