Skip to content

Commit

Permalink
1.3.0 - Add and subtract feature
Browse files Browse the repository at this point in the history
  * Add and subtract money expressions
  * Moved Numeric junk to a Proxy class
  * Small exception handling fixes
  • Loading branch information
Josep M. Bach committed Oct 24, 2010
1 parent 5d97da5 commit 01d99b4
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 55 deletions.
9 changes: 9 additions & 0 deletions README.md
Expand Up @@ -37,6 +37,15 @@ in the past? Just do this:
42.eur.at(Time.parse('2009-09-01')).to_usd
# => 60.12

You can also add an subtract money expressions, which will return a result
converted to the former currency of the expression:

42.eur + 30.usd
# => The same as adding 42 and 30.usd.to_eur

10.gbp + 1.eur
# => The same as adding 10 and 1.eur.to_gbp

##Installation

###Rails 3
Expand Down
2 changes: 1 addition & 1 deletion VERSION
@@ -1 +1 @@
1.2.2
1.3.0
2 changes: 1 addition & 1 deletion lib/core_ext/numeric.rb → lib/core_ext/fixnum.rb
@@ -1,3 +1,3 @@
class Numeric
class Fixnum
include CurrencyConvertible
end
3 changes: 3 additions & 0 deletions lib/core_ext/float.rb
@@ -0,0 +1,3 @@
class Float
include CurrencyConvertible
end
3 changes: 2 additions & 1 deletion lib/simple_currency.rb
@@ -1,2 +1,3 @@
require 'simple_currency/currency_convertible'
require 'core_ext/numeric'
require 'core_ext/fixnum'
require 'core_ext/float'
123 changes: 72 additions & 51 deletions lib/simple_currency/currency_convertible.rb
Expand Up @@ -4,53 +4,82 @@

module CurrencyConvertible

def method_missing(method, *args, &block)
return _from(method.to_s) if method.to_s.length == 3 # Presumably a currency ("eur", "gbp"...)
Operators = {:+ => :add,
:- => :subtract}

# Now capture methods like to_eur, to_gbp, to_usd...
if @original && !(method.to_s =~ /^to_(utc|int|str|ary)/) && method.to_s =~/^to_/ && method.to_s.length == 6
return _to(method.to_s.gsub('to_',''))
end
def add_with_currency(arg)
return add_without_currency(arg) unless arg.is_a? CurrencyConvertible::Proxy
add_without_currency(arg)
end

super(method,*args,&block)
def subtract_with_currency(arg)
return subtract_without_currency(arg) unless arg.is_a? CurrencyConvertible::Proxy
subtract_without_currency(arg)
end

# Historical exchange lookup
def at(exchange = nil)
begin

@exchange_date = exchange.send(:to_date)
rescue
raise "Must use 'at' with a time or date object"
end
self
def self.included(base)
base.send(:alias_method, :add_without_currency, :+)
base.send(:undef_method, :+)
base.send(:alias_method, :+, :add_with_currency)

base.send(:alias_method, :subtract_without_currency, :-)
base.send(:undef_method, :-)
base.send(:alias_method, :-, :subtract_with_currency)
end

private

# Called from first currency metamethod to set the original currency.
#
# 30.eur # => Calls _from and sets @original to 'eur'
#
def _from(currency)
def method_missing(method, *args, &block)
return CurrencyConvertible::Proxy.new(self,method.to_s) if method.to_s.length == 3 # Presumably a currency ("eur", "gbp"...)
super(method,*args,&block)
end

class Proxy
attr_reader :numeric

def initialize(numeric,currency)
@numeric = numeric
@currency = currency
@exchange_date = Time.now.send(:to_date)
@original = currency
end

def method_missing(method, *args, &block)
if !(method.to_s =~ /^to_(utc|int|str|ary)/) && method.to_s =~/^to_/ && method.to_s.length == 6
return _to(method.to_s.gsub('to_',''))
end
@numeric.send(method, *args, &block)
end

# Historical exchange lookup
def at(exchange = nil)
begin
@exchange_date = exchange.send(:to_date)
rescue
raise "Must use 'at' with a time or date object"
end
self
end

# Called from last currency metamethod to set the target currency.
#
# 30.eur.to_usd
# # => Calls _to and returns the final value, say 38.08
#
def +(other)
return @numeric + other unless other.is_a? CurrencyConvertible::Proxy
converted = other.send(:"to_#{@currency}")
@numeric + converted
end

def -(other)
return @numeric - other unless other.is_a? CurrencyConvertible::Proxy
converted = other.send(:"to_#{@currency}")
@numeric - converted
end

private

def _to(target)
raise unless @original # Must be called after a _from have set the @original currency
raise unless @currency

return 0.0 if self == 0 # Obviously
return 0.0 if @numeric == 0 # Obviously

original = @original
original = @currency

amount = self
amount = @numeric

# Check if there's a cached exchange rate for today
return cached_amount(original, target, amount) if cached_rate(original, target)
Expand All @@ -59,16 +88,16 @@ def _to(target)
result = exchange(original, target, amount.abs)

# Cache methods
cache_currency_methods(original, target)
#cache_currency_methods(original, target)

result
end

# Main method (called by _to) which calls Xavier or Xurrency strategies
# Main method (called by _to) which calls Xavier API
# and returns a nice result.
#
def exchange(original, target, amount)
negative = (self < 0)
negative = (@numeric < 0)

# Get the result and round it to 2 decimals
result = sprintf("%.2f", call_xavier_api(original, target, amount)).to_f
Expand Down Expand Up @@ -117,8 +146,10 @@ def call_xavier_api(original, target, amount)
uri = URI.parse(api_url)
retry
else
raise "404 Not Found"
raise NotFoundError.new("404 Not Found")
end
rescue SocketError
raise NotFoundError.new("Socket Error")
end

return nil unless xml_response && parsed_response = Crack::XML.parse(xml_response)
Expand Down Expand Up @@ -152,20 +183,6 @@ def parse_rate(parsed_response, currency)
rate.first['rate'].to_f
end

# Caches currency methods to avoid method missing abuse.
#
def cache_currency_methods(original, target)
# Cache the _from method for faster reuse
self.class.send(:define_method, original.to_sym) do
_from(original)
end unless self.respond_to?(original.to_sym)

# Cache the _to method for faster reuse
self.class.send(:define_method, :"to_#{target}") do
_to(target)
end unless self.respond_to?(:"to_#{target}")
end

##
# Cache helper methods (only useful in a Rails app)
##
Expand Down Expand Up @@ -209,6 +226,7 @@ def cached_amount(original, target, amount)
end
nil
end
end

end

Expand All @@ -222,4 +240,7 @@ class CurrencyNotFoundException < StandardError
class NoRatesFoundException < StandardError
end

class NotFoundError < StandardError
end


43 changes: 42 additions & 1 deletion spec/simple_currency_spec.rb
Expand Up @@ -10,6 +10,44 @@
0.usd.to_eur.should == 0.0
end

describe "operators" do

let(:today) { Time.now }

before(:each) do
mock_xavier_api(today)
end

describe "#+" do
it "adds two money expressions" do
(1.eur + 1.27.usd).should == 2
(1.27.usd + 1.eur).should == 2.54
(1.eur + 1.eur).should == 2
end

it "does not affect non-money expressions" do
(1 + 1.27).should == 2.27
(38.eur + 1.27).should == 39.27
(1.27.usd + 38).should == 39.27
end
end

describe "#-" do
it "subtracts two money expressions" do
(1.eur - 1.27.usd).should == 0
(1.27.usd - 1.eur).should == 0
(1.eur - 1.eur).should == 0
end

it "does not affect non-money expressions" do
(1 - 1.27).should == -0.27
(38.eur - 1.27).should == 36.73
(1.27.usd - 38).should == -36.73
end
end

end

context "using XavierMedia API for exchange" do

let(:today) { Time.now }
Expand Down Expand Up @@ -74,10 +112,12 @@
expect {
begin
1.usd.at(the_past).to_eur
rescue NotFoundError=>e
raise e
rescue Timeout::Error
retry
end
}.to raise_error("404 Not Found")
}.to raise_error(NotFoundError)

end

Expand Down Expand Up @@ -139,4 +179,5 @@

end


end

0 comments on commit 01d99b4

Please sign in to comment.