Ruby - method_missing
-
11-12-2019 - |
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.
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:
Here is what I did...
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 >