Question

I'm trying to implement a method_missing for converting $ to other currencies, as in doing 5.dollars yields 5, 5.yen would yield 0.065 5.euro 6.56 and so on. This I can do now. Now I need to implement it but doing 5.dollars.in(:yen) for example.

This is what I have right now:

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

Can anyone explain how I can do this?

PS: I'd rather you not give me the code, but an explanation, so I can determine on my own how it is done.

Was it helpful?

Solution

Perhaps this will be of more help. It's a working example (note, I'm expecting you to have ActiveSupport [part of Rails] and 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

OTHER TIPS

Added currency 'dollar' and in method:

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

This is more a mathematical problem than computational one.

Each of the @@currencies hash values is normalized to 'dollars': their units are yen/dollar, euro/dollar, rupee/dollar. For 5.euro.in(:yen), you only need to divide euro/dollar by yen/dollar to express the answer as Euros in Yen.

To compute this using Ruby, you leave the method_missing method unchanged and update the class constant to include 'dollar' => 1. Add a Numeric#in method with a single-line computation to solve this problem. That computation needs to apply division in the correct sequence to a floating-point number.

For 5.euro.in(:yen) example, remember that 5.euro is calculated first but will have units of euro/dollar. The in(:yen) method that comes next must be applied to the reciprocal of this number. This will give a number with units in yen/euro, the reciprocal of your desired result.

Wouldn't you just define a method called in that sent the symbol parameter back to 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

So, not quite, because you don't know what the amount currently represents--your method_missing assumes everything is in dollars, even if it isn't.

That's why there's the money gem :)

Rather than using method_missing here, it would be easier to iterate over each of the currencies and define singular and plural methods for them delegating to your conversion method.

I'm assuming you have ActiveSupport here for the sake of convenience. You could do any of this without, but things like constantize and concerns make it easier.

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

My approach to this, based on accepting the limits of the problem posed (extend a method_missing implementation on Numeric, even though as @coreyward indicates this is really the wrong approach for anything not a homework problem) was as follows:

Understanding that 5.euros.in(:yen) can be translated to:

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

what's essentially happening is that we're sending the euros message to the Numeric 5 and then sending the in method to the Numeric result of 5.euros with a parameter of :yen.

In method_missing you should respond to the euros call and return with the result of a euros to dollars conversion, and then (also in method_missing) respond to the in call with the results of converting the dollars (from the previous call) to the symbol passed as a parameter to the in call. That will return the proper value.

Of course you can convert to/from whatever currency you want so long as your conversion factors are correct - with the givens for this particular problem, converting to/from dollars seemed the most sensible.

I am doing this course too and I saw a few examples of how to accomplish the task. At some point self.send was mentioned and I believe someone else has implemented this also but I found this solution to work for me:

https://gist.github.com/2065412

Here is what I did...

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.euro = "+5.euro.to_s
    puts "5.euros = "+5.euros.to_s
    puts "5.dollars.in(:euros) = "+5.dollars.in(:euros).to_s
    puts "10.euros.in(:rupees) = "+10.euros.in(:rupees).to_s
  • Add "'dollar' => 1" into currencies
  • Add a new a argument in method_missing method ", *args"
  • Add a new method "in(arg)" into the Numeric class
  • This method multiply self by the currency specified by the argument "arg"

First, install my units library: gem install sy. Then, define:

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 >

And now you can calculate:

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

You can also define

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

And then

12.usd + 90.cent #=> #<Magnitude: 12.9 >
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top