Skip to content

Commit

Permalink
implement Money::Bank and update VariableExchangeBank to be a subclass
Browse files Browse the repository at this point in the history
of it
  • Loading branch information
semmons99 committed Jul 16, 2010
1 parent 86f78a2 commit 4d688e7
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 80 deletions.
55 changes: 55 additions & 0 deletions lib/money/bank.rb
@@ -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
4 changes: 0 additions & 4 deletions lib/money/errors.rb

This file was deleted.

64 changes: 6 additions & 58 deletions lib/money/variable_exchange_bank.rb
@@ -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
Expand All @@ -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
160 changes: 160 additions & 0 deletions test/bank_spec.rb
@@ -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
33 changes: 15 additions & 18 deletions test/exchange_bank_spec.rb → test/variable_exchange_bank_spec.rb
Expand Up @@ -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
Expand All @@ -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)
Expand Down

0 comments on commit 4d688e7

Please sign in to comment.