Permalink
Browse files

implement Money::Bank and update VariableExchangeBank to be a subclass

of it
  • Loading branch information...
1 parent 86f78a2 commit 4d688e7f2115d5e89e4c0bc9e28865d81dfb854b @semmons99 semmons99 committed Jul 16, 2010
View
@@ -0,0 +1,55 @@
+require 'thread'
+
+class Money
+ class Bank
+ class UnknownRate < StandardError; end
+
+ def self.instance
+ @@singleton
+ end
+
+ @@singleton = Bank.new
+
+ def initialize(&block)
+ @rates = {}
+ @mutex = Mutex.new
+ @rounding_method = block
+ end
+
+ def exchange(cents, from_currency, to_currency, &block)
+ return cents if same_currency?(from_currency, to_currency)
+
+ rate = get_rate(from_currency, to_currency)
+ unless rate
+ raise Money::Bank::UnknownRate, "No conversion rate known for '#{from_currency}' -> '#{to_currency}'"
+ end
+ _from_currency_ = Currency.wrap(from_currency)
+ _to_currency_ = Currency.wrap(to_currency)
+
+ _cents_ = cents / (_from_currency_.subunit_to_unit.to_f / _to_currency_.subunit_to_unit.to_f)
+
+ ex = _cents_ * rate
+ return block.call(ex) if block_given?
+ return @rounding_method.call(ex) unless @rounding_method.nil?
+ ex.to_s.to_i
+ end
+
+ private
+
+ def rate_key_for(from, to)
+ "#{Currency.wrap(from).iso_code}_TO_#{Currency.wrap(to).iso_code}".upcase
+ end
+
+ def set_rate(from, to, rate)
+ @mutex.synchronize{ @rates[rate_key_for(from, to)] = rate }
+ end
+
+ def get_rate(from, to)
+ @mutex.synchronize{ @rates[rate_key_for(from, to)] }
+ end
+
+ def same_currency?(currency1, currency2)
+ Currency.wrap(currency1) == Currency.wrap(currency2)
+ end
+ end
+end
View
@@ -1,4 +0,0 @@
-class Money
- class UnknownRate < StandardError
- end
-end
@@ -1,5 +1,4 @@
-require 'thread'
-require 'money/errors'
+require 'money/bank'
# Class for aiding in exchanging money between different currencies.
# By default, the Money class uses an object of this class (accessible through
@@ -19,64 +18,13 @@
# bank.exchange(100_00, "USD", "CAD") # => 80
#
class Money
- class VariableExchangeBank
- # Returns the singleton instance of VariableExchangeBank.
- #
- # By default, <tt>Money.default_bank</tt> returns the same object.
- def self.instance
- @@singleton
- end
-
- def initialize(&block)
- @rates = {}
- @mutex = Mutex.new
- @rounding_method = block
- end
+ class VariableExchangeBank < Bank
+ @@singleton = VariableExchangeBank.new
- # Registers a conversion rate. +from+ and +to+ are both currency names.
+ # Registers a conversion rate. +from+ and +to+ are both currency names or
+ # +Currency+ objects.
def add_rate(from, to, rate)
- @mutex.synchronize do
- @rates["#{from}_TO_#{to}".upcase] = rate
- end
- end
-
- # Gets the rate for exchanging the currency named +from+ to the currency
- # named +to+. Returns nil if the rate is unknown.
- def get_rate(from, to)
- @mutex.synchronize do
- @rates["#{from}_TO_#{to}".upcase]
- end
- end
-
- # Given two currency names, checks whether they're both the same currency.
- #
- # bank = VariableExchangeBank.new
- # bank.same_currency?("usd", "USD") # => true
- # bank.same_currency?("usd", "EUR") # => false
- def same_currency?(currency1, currency2)
- Currency.wrap(currency1) == Currency.wrap(currency2)
+ set_rate(from, to, rate)
end
-
- # Exchange the given amount of cents in +from_currency+ to +to_currency+.
- # Returns the amount of cents in +to_currency+ as an integer, rounded down.
- #
- # If the conversion rate is unknown, then Money::UnknownRate will be raised.
- def exchange(cents, from_currency, to_currency, &block)
- rate = get_rate(from_currency, to_currency)
- if !rate
- raise Money::UnknownRate, "No conversion rate known for '#{from_currency}' -> '#{to_currency}'"
- end
- _from_currency_ = Currency.wrap(from_currency)
- _to_currency_ = Currency.wrap(to_currency)
-
- _cents_ = cents / (_from_currency_.subunit_to_unit.to_f / _to_currency_.subunit_to_unit.to_f)
-
- ex = _cents_ * rate
- return block.call(ex) if block_given?
- return @rounding_method.call(ex) unless @rounding_method.nil?
- ex.to_s.to_i
- end
-
- @@singleton = VariableExchangeBank.new
end
end
View
@@ -0,0 +1,160 @@
+$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
+require 'money/currency'
+require 'money/bank'
+
+describe Money::Bank do
+ describe '#new without block' do
+ before :each do
+ @bank = Money::Bank.new
+ end
+
+ describe '#rate_key_for' do
+ it 'should accept str/str' do
+ lambda{@bank.send(:rate_key_for, 'USD', 'EUR')}.should_not raise_exception
+ end
+
+ it 'should accept currency/str' do
+ lambda{@bank.send(:rate_key_for, Money::Currency.wrap('USD'), 'EUR')}.should_not raise_exception
+ end
+
+ it 'should accept str/currency' do
+ lambda{@bank.send(:rate_key_for, 'USD', Money::Currency.wrap('EUR'))}.should_not raise_exception
+ end
+
+ it 'should accept currency/currency' do
+ lambda{@bank.send(:rate_key_for, Money::Currency.wrap('USD'), Money::Currency.wrap('EUR'))}.should_not raise_exception
+ end
+
+ it 'should return a hashkey based on the passed arguments' do
+ @bank.send(:rate_key_for, 'USD', 'EUR').should == 'USD_TO_EUR'
+ @bank.send(:rate_key_for, Money::Currency.wrap('USD'), 'EUR').should == 'USD_TO_EUR'
+ @bank.send(:rate_key_for, 'USD', Money::Currency.wrap('EUR')).should == 'USD_TO_EUR'
+ @bank.send(:rate_key_for, Money::Currency.wrap('USD'), Money::Currency.wrap('EUR')).should == 'USD_TO_EUR'
+ end
+
+ it 'should raise an UnknownCurrency exception when an unknown currency is passed' do
+ lambda{@bank.send(:rate_key_for, 'AAA', 'BBB')}.should raise_exception(Money::Currency::UnknownCurrency)
+ end
+ end
+
+ describe '#set_rate' do
+ it 'should set a rate' do
+ @bank.send(:set_rate, 'USD', 'EUR', 1.25)
+ @bank.instance_variable_get(:@rates)['USD_TO_EUR'].should == 1.25
+ end
+
+ it 'should raise an UnknownCurrency exception when an unknown currency is passed' do
+ lambda{@bank.send(:set_rate, 'AAA', 'BBB', 1.25)}.should raise_exception(Money::Currency::UnknownCurrency)
+ end
+ end
+
+ describe '#get_rate' do
+ it 'should return a rate' do
+ @bank.send(:set_rate, 'USD', 'EUR', 1.25)
+ @bank.send(:get_rate, 'USD', 'EUR').should == 1.25
+ end
+
+ it 'should raise an UnknownCurrency exception when an unknown currency is requested' do
+ lambda{@bank.send(:get_rate, 'AAA', 'BBB')}.should raise_exception(Money::Currency::UnknownCurrency)
+ end
+ end
+
+ describe '#same_currency?' do
+ it 'should accept str/str' do
+ lambda{@bank.send(:same_currency?, 'USD', 'EUR')}.should_not raise_exception
+ end
+
+ it 'should accept currency/str' do
+ lambda{@bank.send(:same_currency?, Money::Currency.wrap('USD'), 'EUR')}.should_not raise_exception
+ end
+
+ it 'should accept str/currency' do
+ lambda{@bank.send(:same_currency?, 'USD', Money::Currency.wrap('EUR'))}.should_not raise_exception
+ end
+
+ it 'should accept currency/currency' do
+ lambda{@bank.send(:same_currency?, Money::Currency.wrap('USD'), Money::Currency.wrap('EUR'))}.should_not raise_exception
+ end
+
+ it 'should return `true` when currencies match' do
+ @bank.send(:same_currency?, 'USD', 'USD').should == true
+ @bank.send(:same_currency?, Money::Currency.wrap('USD'), 'USD').should == true
+ @bank.send(:same_currency?, 'USD', Money::Currency.wrap('USD')).should == true
+ @bank.send(:same_currency?, Money::Currency.wrap('USD'), Money::Currency.wrap('USD')).should == true
+ end
+
+ it 'should return `false` when currencies do not match' do
+ @bank.send(:same_currency?, 'USD', 'EUR').should == false
+ @bank.send(:same_currency?, Money::Currency.wrap('USD'), 'EUR').should == false
+ @bank.send(:same_currency?, 'USD', Money::Currency.wrap('EUR')).should == false
+ @bank.send(:same_currency?, Money::Currency.wrap('USD'), Money::Currency.wrap('EUR')).should == false
+ end
+
+ it 'should raise an UnknownCurrency exception when an unknown currency is passed' do
+ lambda{@bank.send(:same_currency?, 'AAA', 'BBB')}.should raise_exception(Money::Currency::UnknownCurrency)
+ end
+ end
+
+ describe '#exchange' do
+ before :each do
+ @bank.send(:set_rate, 'USD', 'EUR', 1.33)
+ end
+
+ it 'should accept str/str' do
+ lambda{@bank.exchange(100, 'USD', 'EUR')}.should_not raise_exception
+ end
+
+ it 'should accept currency/str' do
+ lambda{@bank.exchange(100, Money::Currency.wrap('USD'), 'EUR')}.should_not raise_exception
+ end
+
+ it 'should accept str/currency' do
+ lambda{@bank.exchange(100, 'USD', Money::Currency.wrap('EUR'))}.should_not raise_exception
+ end
+
+ it 'should accept currency/currency' do
+ lambda{@bank.exchange(100, Money::Currency.wrap('USD'), Money::Currency.wrap('EUR'))}.should_not raise_exception
+ end
+
+ it 'should exchange one currency to another' do
+ @bank.exchange(100, 'USD', 'EUR').should == 133
+ end
+
+ it 'should truncate extra digits' do
+ @bank.exchange(10, 'USD', 'EUR').should == 13
+ end
+
+ it 'should raise an UnknownCurrency exception when an unknown currency is requested' do
+ lambda{@bank.exchange(100, 'AAA', 'BBB')}.should raise_exception(Money::Currency::UnknownCurrency)
+ end
+
+ it 'should raise an UnknownRate exception when an unknown rate is requested' do
+ lambda{@bank.exchange(100, 'USD', 'JPY')}.should raise_exception(Money::Bank::UnknownRate)
+ end
+
+ it 'should accept a custom truncation method' do
+ proc = Proc.new{|n| n.ceil}
+ @bank.exchange(10, 'USD', 'EUR', &proc).should == 14
+ end
+ end
+ end
+
+ describe '#new with &block' do
+ before :each do
+ proc = Proc.new{|n| n.ceil}
+ @bank = Money::Bank.new(&proc)
+ @bank.send(:set_rate, 'USD', 'EUR', 1.33)
+ end
+
+ describe '#exchange' do
+ it 'should use a stored truncation method' do
+ @bank.exchange(10, 'USD', 'EUR').should == 14
+ end
+
+ it 'should use a custom truncation method over a stored one' do
+ proc = Proc.new{|n| n.ceil+1}
+ @bank.exchange(10, 'USD', 'EUR', &proc).should == 15
+ end
+ end
+ end
+end
@@ -6,22 +6,19 @@
@bank = Money::VariableExchangeBank.new
end
- it "returns the previously specified conversion rate" do
- @bank.add_rate("USD", "EUR", 0.788332676)
- @bank.add_rate("EUR", "YEN", 122.631477)
- @bank.get_rate("USD", "EUR").should == 0.788332676
- @bank.get_rate("EUR", "YEN").should == 122.631477
- end
+ describe "#add_rate" do
+ it "should add rates correctly" do
+ @bank.add_rate("USD", "EUR", 0.788332676)
+ @bank.add_rate("EUR", "YEN", 122.631477)
- it "treats currency names case-insensitively" do
- @bank.add_rate("usd", "eur", 1)
- @bank.get_rate("USD", "EUR").should == 1
- @bank.same_currency?("USD", "usd").should be_true
- @bank.same_currency?("EUR", "usd").should be_false
- end
-
- it "returns nil if the conversion rate is unknown" do
- @bank.get_rate("American Pesos", "EUR").should be_nil
+ @bank.instance_variable_get(:@rates)['USD_TO_EUR'].should == 0.788332676
+ @bank.instance_variable_get(:@rates)['EUR_TO_JPY'].should == 122.631477
+ end
+
+ it "should treat currency names case-insensitively" do
+ @bank.add_rate("usd", "eur", 1)
+ @bank.instance_variable_get(:@rates)['USD_TO_EUR'].should == 1
+ end
end
it "exchanges money from one currency to another according to the specified conversion rates" do
@@ -38,12 +35,12 @@
@bank.exchange(500_00, "EUR", "YEN").should == 6131573
end
- it "raises Money::UnknownRate upon conversion if the conversion rate is unknown" do
+ it "raises Money::Bank::UnknownRate upon conversion if the conversion rate is unknown" do
block = lambda { @bank.exchange(10, "USD", "EUR") }
- block.should raise_error(Money::UnknownRate)
+ block.should raise_error(Money::Bank::UnknownRate)
end
- describe '.exchange' do
+ describe '#exchange' do
context 'sterling to euros using a rate of 1.39' do
it 'returns the correct amount' do
@bank.add_rate('GBP', 'EUR', 1.38)

0 comments on commit 4d688e7

Please sign in to comment.