From 01d99b451bdfedffe6efa2b0c3a287f5fba1feea Mon Sep 17 00:00:00 2001 From: "Josep M. Bach" Date: Sat, 23 Oct 2010 04:27:33 +0200 Subject: [PATCH] 1.3.0 - Add and subtract feature * Add and subtract money expressions * Moved Numeric junk to a Proxy class * Small exception handling fixes --- README.md | 9 ++ VERSION | 2 +- lib/core_ext/{numeric.rb => fixnum.rb} | 2 +- lib/core_ext/float.rb | 3 + lib/simple_currency.rb | 3 +- lib/simple_currency/currency_convertible.rb | 123 ++++++++++++-------- spec/simple_currency_spec.rb | 43 ++++++- 7 files changed, 130 insertions(+), 55 deletions(-) rename lib/core_ext/{numeric.rb => fixnum.rb} (70%) create mode 100644 lib/core_ext/float.rb diff --git a/README.md b/README.md index 3f1e264..d4f4d79 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/VERSION b/VERSION index 23aa839..f0bb29e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.2 +1.3.0 diff --git a/lib/core_ext/numeric.rb b/lib/core_ext/fixnum.rb similarity index 70% rename from lib/core_ext/numeric.rb rename to lib/core_ext/fixnum.rb index 9714b47..8d07752 100644 --- a/lib/core_ext/numeric.rb +++ b/lib/core_ext/fixnum.rb @@ -1,3 +1,3 @@ -class Numeric +class Fixnum include CurrencyConvertible end diff --git a/lib/core_ext/float.rb b/lib/core_ext/float.rb new file mode 100644 index 0000000..dfdb19d --- /dev/null +++ b/lib/core_ext/float.rb @@ -0,0 +1,3 @@ +class Float + include CurrencyConvertible +end diff --git a/lib/simple_currency.rb b/lib/simple_currency.rb index 907016b..c606fae 100644 --- a/lib/simple_currency.rb +++ b/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' diff --git a/lib/simple_currency/currency_convertible.rb b/lib/simple_currency/currency_convertible.rb index 1e7775e..f17c729 100644 --- a/lib/simple_currency/currency_convertible.rb +++ b/lib/simple_currency/currency_convertible.rb @@ -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) @@ -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 @@ -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) @@ -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) ## @@ -209,6 +226,7 @@ def cached_amount(original, target, amount) end nil end + end end @@ -222,4 +240,7 @@ class CurrencyNotFoundException < StandardError class NoRatesFoundException < StandardError end + class NotFoundError < StandardError + end + diff --git a/spec/simple_currency_spec.rb b/spec/simple_currency_spec.rb index a41e750..e7407c1 100644 --- a/spec/simple_currency_spec.rb +++ b/spec/simple_currency_spec.rb @@ -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 } @@ -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 @@ -139,4 +179,5 @@ end + end