money 1.7.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE CHANGED
@@ -1,4 +1,5 @@
1
1
  Copyright (c) 2005 Tobias Lutke
2
+ Copyright (c) 2008 Phusion
2
3
 
3
4
  Permission is hereby granted, free of charge, to any person obtaining
4
5
  a copy of this software and associated documentation files (the
@@ -17,4 +18,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
18
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
19
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
20
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,97 @@
1
+ = Introduction
2
+
3
+ This library aids one in handling money and different currencies. Features:
4
+
5
+ - Provides a Money class which encapsulates all information about an certain
6
+ amount of money, such as its value and its currency.
7
+ - Represents monetary values as integers, in cents. This avoids floating point
8
+ rounding errors.
9
+ - Provides APIs for exchanging money from one currency to another.
10
+ - Has the ability to parse a money string into a Money object.
11
+
12
+ Resources:
13
+
14
+ - Website: http://money.rubyforge.org
15
+ - RDoc API: http://money.rubyforge.org
16
+ - Git repository: http://github.com/FooBarWidget/money/tree/master
17
+
18
+ == Download
19
+
20
+ Install stable releases with the following command:
21
+
22
+ gem install money
23
+
24
+ The development version (hosted on Github) can be installed with:
25
+
26
+ gem sources -a http://gems.github.com
27
+ gem install FooBarWidget-money
28
+
29
+ == Usage
30
+
31
+ === Synopsis
32
+
33
+ require 'money'
34
+
35
+ # 10.00 USD
36
+ money = Money.new(1000, "USD")
37
+ money.cents # => 1000
38
+ money.currency # => "USD"
39
+
40
+ Money.new(1000, "USD") == Money.new(1000, "USD") # => true
41
+ Money.new(1000, "USD") == Money.new(100, "USD") # => false
42
+ Money.new(1000, "USD") == Money.new(1000, "EUR") # => false
43
+
44
+ === Currency Exchange
45
+
46
+ Exchanging money is performed through an exchange bank object. The default
47
+ exchange bank object requires one to manually specify the exchange rate. Here's
48
+ an example of how it works:
49
+
50
+ Money.add_rate("USD", "CAD", 1.24515)
51
+ Money.add_rate("CAD", "USD", 0.803115)
52
+
53
+ Money.us_dollar(100).exchange_to("CAD") # => Money.new(124, "CAD")
54
+ Money.ca_dollar(100).exchange_to("USD") # => Money.new(80, "USD")
55
+
56
+ Comparison and arithmetic operations work as expected:
57
+
58
+ Money.new(1000, "USD") <=> Money.new(900, "USD") # => 1; 9.00 USD is smaller
59
+ Money.new(1000, "EUR") + Money.new(10, "EUR") == Money.new(1010, "EUR")
60
+
61
+ Money.add_rate("USD", "EUR", 0.5)
62
+ Money.new(1000, "EUR") + Money.new(1000, "USD") == Money.new(1500, "EUR")
63
+
64
+ There is nothing stopping you from creating bank objects which scrapes
65
+ www.xe.com for the current rates or just returns <tt>rand(2)</tt>:
66
+
67
+ Money.default_bank = ExchangeBankWhichScrapesXeDotCom.new
68
+
69
+ === Ruby on Rails
70
+
71
+ Use the +compose_of+ helper to let Active Record deal with embedding the money
72
+ object in your models. The following example requires a +cents+ and a +currency+
73
+ field.
74
+
75
+ class ProductUnit < ActiveRecord::Base
76
+ belongs_to :product
77
+ composed_of :price, :class_name => "Money", :mapping => [%w(cents cents), %w(currency currency)]
78
+
79
+ private
80
+ validate :cents_not_zero
81
+
82
+ def cents_not_zero
83
+ errors.add("cents", "cannot be zero or less") unless cents > 0
84
+ end
85
+
86
+ validates_presence_of :sku, :currency
87
+ validates_uniqueness_of :sku
88
+ end
89
+
90
+ === Default Currency
91
+
92
+ By default Money defaults to USD as its currency. This can be overwritten using
93
+
94
+ Money.default_currency = "CAD"
95
+
96
+ If you use Rails, then environment.rb is a very good place to put this.
97
+
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ desc "Build a gem"
2
+ task :gem do
3
+ sh "gem build money.gemspec"
4
+ end
5
+
6
+ task "Generate RDoc documentation"
7
+ task :rdoc do
8
+ sh "hanna README.rdoc lib -U"
9
+ end
10
+
11
+ task :upload => :rdoc do
12
+ sh "scp -r doc/* rubyforge.org:/var/www/gforge-projects/money/"
13
+ end
14
+
15
+ desc "Run unit tests"
16
+ task :test do
17
+ sh "spec -f s -c test/*_spec.rb"
18
+ end
data/lib/money.rb CHANGED
@@ -1,5 +1,5 @@
1
- #--
2
1
  # Copyright (c) 2005 Tobias Luetke
2
+ # Copyright (c) 2008 Phusion
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -19,11 +19,7 @@
19
19
  # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
20
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
- #++
23
22
 
24
-
25
- require File.dirname(__FILE__) + '/support/cattr_accessor'
26
- require File.dirname(__FILE__) + '/bank/no_exchange_bank'
27
- require File.dirname(__FILE__) + '/bank/variable_exchange_bank'
28
- require File.dirname(__FILE__) + '/money/money'
29
- require File.dirname(__FILE__) + '/money/core_extensions'
23
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__))
24
+ require 'money/money'
25
+ require 'money/core_extensions'
@@ -1,25 +1,37 @@
1
- # Allows Writing of 100.to_money for +Numeric+ types
2
- # 100.to_money => #<Money @cents=10000>
3
- # 100.37.to_money => #<Money @cents=10037>
4
1
  class Numeric
2
+ # Converts this numeric to a Money object in the default currency. It
3
+ # multiplies the numeric value by 100 and treats that as cents.
4
+ #
5
+ # 100.to_money => #<Money @cents=10000>
6
+ # 100.37.to_money => #<Money @cents=10037>
5
7
  def to_money
6
8
  Money.new(self * 100)
7
9
  end
8
10
  end
9
11
 
10
- # Allows Writing of '100'.to_money for +String+ types
11
- # Excess characters will be discarded
12
- # '100'.to_money => #<Money @cents=10000>
13
- # '100.37'.to_money => #<Money @cents=10037>
14
12
  class String
13
+ # Parses the current string and converts it to a Money object.
14
+ # Excess characters will be discarded.
15
+ #
16
+ # '100'.to_money # => #<Money @cents=10000>
17
+ # '100.37'.to_money # => #<Money @cents=10037>
18
+ # '100 USD'.to_money # => #<Money @cents=10000, @currency="USD">
19
+ # 'USD 100'.to_money # => #<Money @cents=10000, @currency="USD">
20
+ # '$100 USD'.to_money # => #<Money @cents=10000, @currency="USD">
15
21
  def to_money
16
- # Get the currency
22
+ # Get the currency.
17
23
  matches = scan /([A-Z]{2,3})/
18
24
  currency = matches[0] ? matches[0][0] : Money.default_currency
19
25
 
20
26
  # Get the cents amount
21
- matches = scan /(\-?\d+(\.(\d+))?)/
22
- cents = matches[0] ? (matches[0][0].to_f * 100) : 0
27
+ matches = scan /(\-?[\d ]+([\.,](\d+))?)/
28
+ cents = if matches[0]
29
+ value = matches[0][0].gsub(/,/, '.')
30
+ value.gsub!(/ +/, '')
31
+ value.to_f * 100
32
+ else
33
+ 0
34
+ end
23
35
 
24
36
  Money.new(cents, currency)
25
37
  end
@@ -0,0 +1,4 @@
1
+ class Money
2
+ class UnknownRate < StandardError
3
+ end
4
+ end
data/lib/money/money.rb CHANGED
@@ -1,64 +1,89 @@
1
- # === Usage with ActiveRecord
2
- #
3
- # Use the compose_of helper to let active record deal with embedding the money
4
- # object in your models. The following example requires a cents and a currency field.
5
- #
6
- # class ProductUnit < ActiveRecord::Base
7
- # belongs_to :product
8
- # composed_of :price, :class_name => "Money", :mapping => [ %w(cents cents), %w(currency currency) ]
9
- #
10
- # private
11
- # validate :cents_not_zero
12
- #
13
- # def cents_not_zero
14
- # errors.add("cents", "cannot be zero or less") unless cents > 0
15
- # end
16
- #
17
- # validates_presence_of :sku, :currency
18
- # validates_uniqueness_of :sku
19
- # end
20
- #
1
+ require 'money/variable_exchange_bank'
2
+
3
+ # Represents an amount of money in a certain currency.
21
4
  class Money
22
5
  include Comparable
23
-
24
- attr_reader :cents, :currency
25
-
26
- class MoneyError < StandardError# :nodoc:
6
+
7
+ attr_reader :cents, :currency, :bank
8
+
9
+ class << self
10
+ # Each Money object is associated to a bank object, which is responsible
11
+ # for currency exchange. This property allows one to specify the default
12
+ # bank object.
13
+ #
14
+ # bank1 = MyBank.new
15
+ # bank2 = MyOtherBank.new
16
+ #
17
+ # Money.default_bank = bank1
18
+ # money1 = Money.new(10)
19
+ # money1.bank # => bank1
20
+ #
21
+ # Money.default_bank = bank2
22
+ # money2 = Money.new(10)
23
+ # money2.bank # => bank2
24
+ # money1.bank # => bank1
25
+ #
26
+ # The default value for this property is an instance if VariableExchangeBank.
27
+ # It allows one to specify custom exchange rates:
28
+ #
29
+ # Money.default_bank.add_rate("USD", "CAD", 1.24515)
30
+ # Money.default_bank.add_rate("CAD", "USD", 0.803115)
31
+ # Money.us_dollar(100).exchange_to("CAD") # => Money.ca_dollar(124)
32
+ # Money.ca_dollar(100).exchange_to("USD") # => Money.us_dollar(80)
33
+ attr_accessor :default_bank
34
+
35
+ # The default currency, which is used when <tt>Money.new</tt> is called
36
+ # without an explicit currency argument. The default value is "USD".
37
+ attr_accessor :default_currency
38
+ end
39
+
40
+ self.default_bank = VariableExchangeBank.instance
41
+ self.default_currency = "USD"
42
+
43
+
44
+ # Create a new money object with value 0.
45
+ def self.empty(currency = default_currency)
46
+ Money.new(0, currency)
27
47
  end
28
48
 
29
- # Bank lets you exchange the object which is responsible for currency
30
- # exchange.
31
- # The default implementation just throws an exception. However money
32
- # ships with a variable exchange bank implementation which supports
33
- # custom excahnge rates:
34
- #
35
- # Money.bank = VariableExchangeBank.new
36
- # Money.bank.add_rate("USD", "CAD", 1.24515)
37
- # Money.bank.add_rate("CAD", "USD", 0.803115)
38
- # Money.us_dollar(100).exchange_to("CAD") => Money.ca_dollar(124)
39
- # Money.ca_dollar(100).exchange_to("USD") => Money.us_dollar(80)
40
- @@bank = NoExchangeBank.new
41
- cattr_accessor :bank
42
-
43
- @@default_currency = "USD"
44
- cattr_accessor :default_currency
49
+ # Creates a new Money object of the given value, using the Canadian dollar currency.
50
+ def self.ca_dollar(cents)
51
+ Money.new(cents, "CAD")
52
+ end
45
53
 
54
+ # Creates a new Money object of the given value, using the American dollar currency.
55
+ def self.us_dollar(cents)
56
+ Money.new(cents, "USD")
57
+ end
58
+
59
+ # Creates a new Money object of the given value, using the Euro currency.
60
+ def self.euro(cents)
61
+ Money.new(cents, "EUR")
62
+ end
63
+
64
+ def self.add_rate(from_currency, to_currency, rate)
65
+ Money.default_bank.add_rate(from_currency, to_currency, rate)
66
+ end
67
+
68
+
46
69
  # Creates a new money object.
47
70
  # Money.new(100)
48
71
  #
49
72
  # Alternativly you can use the convinience methods like
50
73
  # Money.ca_dollar and Money.us_dollar
51
- def initialize(cents, currency = default_currency)
52
- @cents, @currency = cents.round, currency
74
+ def initialize(cents, currency = Money.default_currency, bank = Money.default_bank)
75
+ @cents = cents.round
76
+ @currency = currency
77
+ @bank = bank
53
78
  end
54
79
 
55
80
  # Do two money objects equal? Only works if both objects are of the same currency
56
- def eql?(other_money)
57
- cents == other_money.cents && currency == other_money.currency
81
+ def ==(other_money)
82
+ cents == other_money.cents && bank.same_currency?(currency, other_money.currency)
58
83
  end
59
84
 
60
85
  def <=>(other_money)
61
- if currency == other_money.currency
86
+ if bank.same_currency?(currency, other_money.currency)
62
87
  cents <=> other_money.cents
63
88
  else
64
89
  cents <=> other_money.exchange_to(currency).cents
@@ -66,9 +91,6 @@ class Money
66
91
  end
67
92
 
68
93
  def +(other_money)
69
- return other_money.dup if cents.zero?
70
- return dup if other_money.cents.zero?
71
-
72
94
  if currency == other_money.currency
73
95
  Money.new(cents + other_money.cents, other_money.currency)
74
96
  else
@@ -77,24 +99,16 @@ class Money
77
99
  end
78
100
 
79
101
  def -(other_money)
80
-
81
102
  if currency == other_money.currency
82
103
  Money.new(cents - other_money.cents, other_money.currency)
83
104
  else
84
-
85
- return other_money.dup if self.cents.zero?
86
-
87
- return self.dup if other_money.cents.zero?
88
-
89
-
90
105
  Money.new(cents - other_money.exchange_to(currency).cents, currency)
91
-
92
106
  end
93
107
  end
94
108
 
95
109
  # get the cents value of the object
96
110
  def cents
97
- @cents.to_i
111
+ @cents
98
112
  end
99
113
 
100
114
  # multiply money by fixnum
@@ -153,57 +167,37 @@ class Money
153
167
  end
154
168
  formatted
155
169
  end
156
-
170
+
157
171
  # Money.ca_dollar(100).to_s => "1.00"
158
172
  def to_s
159
- sprintf("%.2f", cents.to_f / 100 )
173
+ sprintf("%.2f", cents / 100.00)
160
174
  end
161
-
162
- # Recieve the amount of this money object in another currency
175
+
176
+ # Recieve the amount of this money object in another currency.
163
177
  def exchange_to(other_currency)
164
- self.class.bank.reduce(self, other_currency)
178
+ Money.new(@bank.exchange(self.cents, currency, other_currency), other_currency)
165
179
  end
166
-
167
- # Create a new money object with value 0
168
- def self.empty(currency = default_currency)
169
- Money.new(0, currency)
170
- end
171
-
172
- # Create a new money object using the Canadian dollar currency
173
- def self.ca_dollar(num)
174
- Money.new(num, "CAD")
175
- end
176
-
177
- # Create a new money object using the American dollar currency
178
- def self.us_dollar(num)
179
- Money.new(num, "USD")
180
- end
181
-
182
- # Create a new money object using the Euro currency
183
- def self.euro(num)
184
- Money.new(num, "EUR")
185
- end
186
-
180
+
187
181
  # Recieve a money object with the same amount as the current Money object
188
182
  # in american dollar
189
183
  def as_us_dollar
190
184
  exchange_to("USD")
191
185
  end
192
-
186
+
193
187
  # Recieve a money object with the same amount as the current Money object
194
188
  # in canadian dollar
195
189
  def as_ca_dollar
196
190
  exchange_to("CAD")
197
191
  end
198
-
192
+
199
193
  # Recieve a money object with the same amount as the current Money object
200
194
  # in euro
201
- def as_ca_euro
195
+ def as_euro
202
196
  exchange_to("EUR")
203
- end
204
-
197
+ end
198
+
205
199
  # Conversation to self
206
200
  def to_money
207
201
  self
208
202
  end
209
- end
203
+ end
@@ -0,0 +1,72 @@
1
+ require 'thread'
2
+ require 'money/errors'
3
+
4
+ # Class for aiding in exchanging money between different currencies.
5
+ # By default, the Money class uses an object of this class (accessible through
6
+ # Money#bank) for performing currency exchanges.
7
+ #
8
+ # By default, VariableExchangeBank has no knowledge about conversion rates.
9
+ # One must manually specify them with +add_rate+, after which one can perform
10
+ # exchanges with +exchange+. For example:
11
+ #
12
+ # bank = Money::VariableExchangeBank.new
13
+ # bank.add_rate("USD", "CAD", 1.24515)
14
+ # bank.add_rate("CAD", "USD", 0.803115)
15
+ #
16
+ # # Exchange 100 CAD to USD:
17
+ # bank.exchange(100_00, "CAD", "USD") # => 124
18
+ # # Exchange 100 USD to CAD:
19
+ # bank.exchange(100_00, "USD", "CAD") # => 80
20
+ class Money
21
+ class VariableExchangeBank
22
+ # Returns the singleton instance of VariableExchangeBank.
23
+ #
24
+ # By default, <tt>Money.default_bank</tt> returns the same object.
25
+ def self.instance
26
+ @@singleton
27
+ end
28
+
29
+ def initialize
30
+ @rates = {}
31
+ @mutex = Mutex.new
32
+ end
33
+
34
+ # Registers a conversion rate. +from+ and +to+ are both currency names.
35
+ def add_rate(from, to, rate)
36
+ @mutex.synchronize do
37
+ @rates["#{from}_TO_#{to}".upcase] = rate
38
+ end
39
+ end
40
+
41
+ # Gets the rate for exchanging the currency named +from+ to the currency
42
+ # named +to+. Returns nil if the rate is unknown.
43
+ def get_rate(from, to)
44
+ @mutex.synchronize do
45
+ @rates["#{from}_TO_#{to}".upcase]
46
+ end
47
+ end
48
+
49
+ # Given two currency names, checks whether they're both the same currency.
50
+ #
51
+ # bank = VariableExchangeBank.new
52
+ # bank.same_currency?("usd", "USD") # => true
53
+ # bank.same_currency?("usd", "EUR") # => false
54
+ def same_currency?(currency1, currency2)
55
+ currency1.upcase == currency2.upcase
56
+ end
57
+
58
+ # Exchange the given amount of cents in +from_currency+ to +to_currency+.
59
+ # Returns the amount of cents in +to_currency+ as an integer, rounded down.
60
+ #
61
+ # If the conversion rate is unknown, then Money::UnknownRate will be raised.
62
+ def exchange(cents, from_currency, to_currency)
63
+ rate = get_rate(from_currency, to_currency)
64
+ if !rate
65
+ raise Money::UnknownRate, "No conversion rate known for '#{from_currency}' -> '#{to_currency}'"
66
+ end
67
+ (cents * rate).floor
68
+ end
69
+
70
+ @@singleton = VariableExchangeBank.new
71
+ end
72
+ end
data/money.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "money"
3
+ s.version = "2.0.0"
4
+ s.summary = "Money and currency exchange support library"
5
+ s.email = "hongli@phusion.nl"
6
+ s.homepage = "http://money.rubyforge.org/"
7
+ s.description = "Money and currency exchange support library."
8
+ s.has_rdoc = true
9
+ s.rubyforge_project = "money"
10
+ s.authors = ["Tobias Luetke", "Hongli Lai"]
11
+
12
+ s.files = [
13
+ "README.rdoc", "MIT-LICENSE", "money.gemspec", "Rakefile",
14
+ "lib/money.rb",
15
+ "lib/money/core_extensions.rb",
16
+ "lib/money/errors.rb",
17
+ "lib/money/money.rb",
18
+ "lib/money/variable_exchange_bank.rb",
19
+ "test/core_extensions_spec.rb",
20
+ "test/exchange_bank_spec.rb",
21
+ "test/money_spec.rb"
22
+ ]
23
+ end
@@ -0,0 +1,33 @@
1
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__) + "/../lib")
2
+ require 'money/core_extensions'
3
+
4
+ describe "Money core extensions" do
5
+ specify "Numberic#to_money works" do
6
+ money = 1234.to_money
7
+ money.cents.should == 1234_00
8
+ money.currency.should == Money.default_currency
9
+
10
+ money = 100.37.to_money
11
+ money.cents.should == 100_37
12
+ money.currency.should == Money.default_currency
13
+ end
14
+
15
+ specify "String#to_money works" do
16
+ "100".to_money.should == Money.new(100_00)
17
+ "100.37".to_money.should == Money.new(100_37)
18
+ "100,37".to_money.should == Money.new(100_37)
19
+ "100 000".to_money.should == Money.new(100_000_00)
20
+
21
+ "100 USD".to_money.should == Money.new(100_00, "USD")
22
+ "100 EUR".to_money.should == Money.new(100_00, "EUR")
23
+ "100.37 EUR".to_money.should == Money.new(100_37, "EUR")
24
+ "100,37 EUR".to_money.should == Money.new(100_37, "EUR")
25
+
26
+ "USD 100".to_money.should == Money.new(100_00, "USD")
27
+ "EUR 100".to_money.should == Money.new(100_00, "EUR")
28
+ "EUR 100.37".to_money.should == Money.new(100_37, "EUR")
29
+ "EUR 100,37".to_money.should == Money.new(100_37, "EUR")
30
+
31
+ "$100 USD".to_money.should == Money.new(100_00, "USD")
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__) + "/../lib")
2
+ require 'money/variable_exchange_bank'
3
+
4
+ describe Money::VariableExchangeBank do
5
+ before :each do
6
+ @bank = Money::VariableExchangeBank.new
7
+ end
8
+
9
+ it "returns the previously specified conversion rate" do
10
+ @bank.add_rate("USD", "EUR", 0.788332676)
11
+ @bank.add_rate("EUR", "YEN", 122.631477)
12
+ @bank.get_rate("USD", "EUR").should == 0.788332676
13
+ @bank.get_rate("EUR", "YEN").should == 122.631477
14
+ end
15
+
16
+ it "treats currency names case-insensitively" do
17
+ @bank.add_rate("usd", "eur", 1)
18
+ @bank.get_rate("USD", "EUR").should == 1
19
+ @bank.same_currency?("USD", "usd").should be_true
20
+ @bank.same_currency?("EUR", "usd").should be_false
21
+ end
22
+
23
+ it "returns nil if the conversion rate is unknown" do
24
+ @bank.get_rate("American Pesos", "EUR").should be_nil
25
+ end
26
+
27
+ it "exchanges money from one currency to another according to the specified conversion rates" do
28
+ @bank.add_rate("USD", "EUR", 0.5)
29
+ @bank.add_rate("EUR", "YEN", 10)
30
+ @bank.exchange(10_00, "USD", "EUR").should == 5_00
31
+ @bank.exchange(500_00, "EUR", "YEN").should == 5000_00
32
+ end
33
+
34
+ it "rounds the exchanged result down" do
35
+ @bank.add_rate("USD", "EUR", 0.788332676)
36
+ @bank.add_rate("EUR", "YEN", 122.631477)
37
+ @bank.exchange(10_00, "USD", "EUR").should == 788
38
+ @bank.exchange(500_00, "EUR", "YEN").should == 6131573
39
+ end
40
+
41
+ it "raises Money::UnknownRate upon conversion if the conversion rate is unknown" do
42
+ block = lambda { @bank.exchange(10, "USD", "EUR") }
43
+ block.should raise_error(Money::UnknownRate)
44
+ end
45
+ end
@@ -0,0 +1,124 @@
1
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__) + "/../lib")
2
+ require 'money/money'
3
+
4
+ describe Money do
5
+ it "is associated to the singleton instance of VariableExchangeBank by default" do
6
+ Money.new(0).bank.object_id.should == Money::VariableExchangeBank.instance.object_id
7
+ end
8
+
9
+ specify "#cents returns the amount of cents passed to the constructor" do
10
+ Money.new(200_00, "USD").cents.should == 200_00
11
+ end
12
+
13
+ it "rounds the given cents to an integer" do
14
+ Money.new(1.00, "USD").cents.should == 1
15
+ Money.new(1.01, "USD").cents.should == 1
16
+ Money.new(1.50, "USD").cents.should == 2
17
+ end
18
+
19
+ specify "#currency returns the currency passed to the constructor" do
20
+ Money.new(200_00, "USD").currency.should == "USD"
21
+ end
22
+
23
+ specify "#zero? returns whether the amount is 0" do
24
+ Money.new(0, "USD").should be_zero
25
+ Money.new(0, "EUR").should be_zero
26
+ Money.new(1, "USD").should_not be_zero
27
+ Money.new(10, "YEN").should_not be_zero
28
+ Money.new(-1, "EUR").should_not be_zero
29
+ end
30
+
31
+ specify "#exchange_to exchanges the amount via its exchange bank" do
32
+ money = Money.new(100_00, "USD")
33
+ money.bank.should_receive(:exchange).with(100_00, "USD", "EUR").and_return(200_00)
34
+ money.exchange_to("EUR")
35
+ end
36
+
37
+ specify "#exchange_to exchanges the amount properly" do
38
+ money = Money.new(100_00, "USD")
39
+ money.bank.should_receive(:exchange).with(100_00, "USD", "EUR").and_return(200_00)
40
+ money.exchange_to("EUR").should == Money.new(200_00, "EUR")
41
+ end
42
+
43
+ specify "#== returns true if and only if their amount and currency are equal" do
44
+ Money.new(1_00, "USD").should == Money.new(1_00, "USD")
45
+ Money.new(1_00, "USD").should_not == Money.new(1_00, "EUR")
46
+ Money.new(1_00, "USD").should_not == Money.new(2_00, "USD")
47
+ Money.new(1_00, "USD").should_not == Money.new(99_00, "EUR")
48
+ end
49
+
50
+ specify "#* multiplies the money's amount by the multiplier while retaining the currency" do
51
+ (Money.new(1_00, "USD") * 10).should == Money.new(10_00, "USD")
52
+ end
53
+
54
+ specify "#* divides the money's amount by the divisor while retaining the currency" do
55
+ (Money.new(10_00, "USD") / 10).should == Money.new(1_00, "USD")
56
+ end
57
+
58
+ specify "Money.empty creates a new Money object of 0 cents" do
59
+ Money.empty.should == Money.new(0)
60
+ end
61
+
62
+ specify "Money.ca_dollar creates a new Money object of the given value in CAD" do
63
+ Money.ca_dollar(50).should == Money.new(50, "CAD")
64
+ end
65
+
66
+ specify "Money.ca_dollar creates a new Money object of the given value in USD" do
67
+ Money.us_dollar(50).should == Money.new(50, "USD")
68
+ end
69
+
70
+ specify "Money.ca_dollar creates a new Money object of the given value in EUR" do
71
+ Money.euro(50).should == Money.new(50, "EUR")
72
+ end
73
+
74
+ specify "Money.add_rate works" do
75
+ Money.add_rate("EUR", "USD", 10)
76
+ Money.new(10_00, "EUR").exchange_to("USD").should == Money.new(100_00, "USD")
77
+ end
78
+ end
79
+
80
+ describe "Actions involving two Money objects" do
81
+ describe "if the other Money object has the same currency" do
82
+ specify "#<=> compares the two objects' amounts" do
83
+ (Money.new(1_00, "USD") <=> Money.new(1_00, "USD")).should == 0
84
+ (Money.new(1_00, "USD") <=> Money.new(99, "USD")).should > 0
85
+ (Money.new(1_00, "USD") <=> Money.new(2_00, "USD")).should < 0
86
+ end
87
+
88
+ specify "#+ adds the other object's amount to the current object's amount while retaining the currency" do
89
+ (Money.new(10_00, "USD") + Money.new(90, "USD")).should == Money.new(10_90, "USD")
90
+ end
91
+
92
+ specify "#- substracts the other object's amount from the current object's amount while retaining the currency" do
93
+ (Money.new(10_00, "USD") - Money.new(90, "USD")).should == Money.new(9_10, "USD")
94
+ end
95
+ end
96
+
97
+ describe "if the other Money object has a different currency" do
98
+ specify "#<=> compares the two objects' amount after converting the other object's amount to its own currency" do
99
+ target = Money.new(200_00, "EUR")
100
+ target.should_receive(:exchange_to).with("USD").and_return(Money.new(300_00, "USD"))
101
+ (Money.new(100_00, "USD") <=> target).should < 0
102
+
103
+ target = Money.new(200_00, "EUR")
104
+ target.should_receive(:exchange_to).with("USD").and_return(Money.new(100_00, "USD"))
105
+ (Money.new(100_00, "USD") <=> target).should == 0
106
+
107
+ target = Money.new(200_00, "EUR")
108
+ target.should_receive(:exchange_to).with("USD").and_return(Money.new(99_00, "USD"))
109
+ (Money.new(100_00, "USD") <=> target).should > 0
110
+ end
111
+
112
+ specify "#+ adds the other object's amount, converted to this object's currency, to this object's amount while retaining its currency" do
113
+ other = Money.new(90, "EUR")
114
+ other.should_receive(:exchange_to).with("USD").and_return(Money.new(9_00, "USD"))
115
+ (Money.new(10_00, "USD") + other).should == Money.new(19_00, "USD")
116
+ end
117
+
118
+ specify "#- substracts the other object's amount, converted to this object's currency, from this object's amount while retaining its currency" do
119
+ other = Money.new(90, "EUR")
120
+ other.should_receive(:exchange_to).with("USD").and_return(Money.new(9_00, "USD"))
121
+ (Money.new(10_00, "USD") - other).should == Money.new(1_00, "USD")
122
+ end
123
+ end
124
+ end
metadata CHANGED
@@ -1,49 +1,65 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.8.11
3
- specification_version: 1
4
2
  name: money
5
3
  version: !ruby/object:Gem::Version
6
- version: 1.7.1
7
- date: 2006-02-13 00:00:00 -05:00
8
- summary: Class aiding in the handling of Money.
9
- require_paths:
10
- - lib
11
- email: tobi@leetsoft.com
12
- homepage: http://leetsoft.com/rails/money
13
- rubyforge_project:
14
- description: Class aiding in the handling of Money and Currencies.\nIt supports easy pluggable bank objects for customized exchange strategies.\nCan be used as composite in ActiveRecord tables.
15
- autorequire: money
16
- default_executable:
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Tobias Luetke
8
+ - Hongli Lai
9
+ autorequire:
17
10
  bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2008-10-31 00:00:00 +01:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: Money and currency exchange support library.
18
+ email: hongli@phusion.nl
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files: []
24
+
25
+ files:
26
+ - README.rdoc
27
+ - MIT-LICENSE
28
+ - money.gemspec
29
+ - Rakefile
30
+ - lib/money.rb
31
+ - lib/money/core_extensions.rb
32
+ - lib/money/errors.rb
33
+ - lib/money/money.rb
34
+ - lib/money/variable_exchange_bank.rb
35
+ - test/core_extensions_spec.rb
36
+ - test/exchange_bank_spec.rb
37
+ - test/money_spec.rb
18
38
  has_rdoc: true
19
- required_ruby_version: !ruby/object:Gem::Version::Requirement
39
+ homepage: http://money.rubyforge.org/
40
+ post_install_message:
41
+ rdoc_options: []
42
+
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
20
46
  requirements:
21
- -
22
- - ">"
23
- - !ruby/object:Gem::Version
24
- version: 0.0.0
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
25
50
  version:
26
- platform: ruby
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ requirements: []
58
+
59
+ rubyforge_project: money
60
+ rubygems_version: 1.2.0
27
61
  signing_key:
28
- cert_chain:
29
- authors:
30
- - Tobias Luetke
31
- files:
32
- - README
33
- - MIT-LICENSE
34
- - lib/bank
35
- - lib/money
36
- - lib/money.rb
37
- - lib/support
38
- - lib/bank/no_exchange_bank.rb
39
- - lib/bank/variable_exchange_bank.rb
40
- - lib/money/core_extensions.rb
41
- - lib/money/money.rb
42
- - lib/support/cattr_accessor.rb
62
+ specification_version: 2
63
+ summary: Money and currency exchange support library
43
64
  test_files: []
44
- rdoc_options: []
45
- extra_rdoc_files: []
46
- executables: []
47
- extensions: []
48
- requirements: []
49
- dependencies: []
65
+
data/README DELETED
@@ -1,75 +0,0 @@
1
- == Money class
2
-
3
- This money class is based on the example from the ActiveRecord doc:
4
- http://api.rubyonrails.org/classes/ActiveRecord/Aggregations/ClassMethods.html
5
-
6
- Its in production use at http://www.snowdevil.ca and I haven't found any major issues
7
- so far.
8
- The main reason to open source it is because It might be useful to other people and
9
- I hope i'll get some feedback on how to improve the class.
10
-
11
- I bundled the exporter with the money class since some tests depend on it and I figured
12
- that most applications which need to deal with Money also need to deal with proper
13
- exporting.
14
-
15
- == Download
16
-
17
- Preferred method of installation is gem:
18
-
19
- gem install --source http://dist.leetsoft.com money
20
-
21
- Alternatively you can get the library packed
22
-
23
- http://dist.leetsoft.com/pkg/
24
-
25
- == Usage
26
-
27
- Use the compose_of helper to let active record deal with embedding the money
28
- object in your models. The following example requires a cents and a currency field.
29
-
30
- class ProductUnit < ActiveRecord::Base
31
- belongs_to :product
32
- composed_of :price, :class_name => "Money", :mapping => [%w(cents cents) %(currency currency)]
33
-
34
- private
35
- validate :cents_not_zero
36
-
37
- def cents_not_zero
38
- errors.add("cents", "cannot be zero or less") unless cents > 0
39
- end
40
-
41
- validates_presence_of :sku, :currency
42
- validates_uniqueness_of :sku
43
- end
44
-
45
- == Class configuration
46
-
47
- Two const class variables are available to tailor Money to your needs.
48
- If you don't need currency exchange at all, just ignore those.
49
-
50
- === Default Currency
51
-
52
- By default Money defaults to USD as its currency. This can be overwritten using
53
-
54
- Money.default_currency = "CAD"
55
-
56
- If you use rails, the environment.rb is a very good place to put this.
57
-
58
- === Currency Exchange
59
-
60
- The second parameter is a bit more complex. It lets you provide your own implementation of the
61
- currency exchange service. By default Money throws an exception when trying to call .exchange_to.
62
-
63
- A second minimalist implementation is provided which lets you supply custom exchange rates:
64
-
65
- Money.bank = VariableExchangeBank.new
66
- Money.bank.add_rate("USD", "CAD", 1.24515)
67
- Money.bank.add_rate("CAD", "USD", 0.803115)
68
- Money.us_dollar(100).exchange_to("CAD") => Money.ca_dollar(124)
69
- Money.ca_dollar(100).exchange_to("USD") => Money.us_dollar(80)
70
-
71
- There is nothing stopping you from creating bank objects which scrape www.xe.com for the current rates or just return rand(2)
72
-
73
- == Code
74
-
75
- If you have any improvements please email them to tobi [at] leetsoft.com
@@ -1,9 +0,0 @@
1
- class NoExchangeBank# :nodoc:
2
-
3
- def reduce(money, currency)
4
- return money if money.currency == currency
5
- raise Money::MoneyError.new("Current Money::bank does not support money exchange. Please implement a bank object that does and assign it to the Money class.")
6
- end
7
-
8
-
9
- end
@@ -1,30 +0,0 @@
1
- # Example useage:
2
- #
3
- # Money.bank = VariableExchangeBank.new
4
- # Money.bank.add_rate("USD", "CAD", 1.24515)
5
- # Money.bank.add_rate("CAD", "USD", 0.803115)
6
- # Money.us_dollar(100).exchange_to("CAD") => Money.ca_dollar(124)
7
- # Money.ca_dollar(100).exchange_to("USD") => Money.us_dollar(80)
8
- class VariableExchangeBank
9
-
10
- def add_rate(from, to, rate)
11
- rates["#{from}_TO_#{to}".upcase] = rate
12
- end
13
-
14
- def get_rate(from, to)
15
- rates["#{from}_TO_#{to}".upcase]
16
- end
17
-
18
- def reduce(money, currency)
19
- rate = get_rate(money.currency, currency) or raise Money::MoneyError.new("Can't find required exchange rate")
20
-
21
- Money.new((money.cents * rate).floor, currency)
22
- end
23
-
24
- private
25
-
26
- def rates
27
- @rates ||= {}
28
- end
29
-
30
- end
@@ -1,57 +0,0 @@
1
- # Extends the class object with class and instance accessors for class attributes,
2
- # just like the native attr* accessors for instance attributes.
3
- class Class # :nodoc:
4
- def cattr_reader(*syms)
5
- syms.each do |sym|
6
- class_eval <<-EOS
7
- if ! defined? @@#{sym.id2name}
8
- @@#{sym.id2name} = nil
9
- end
10
-
11
- def self.#{sym.id2name}
12
- @@#{sym}
13
- end
14
-
15
- def #{sym.id2name}
16
- @@#{sym}
17
- end
18
-
19
- def call_#{sym.id2name}
20
- case @@#{sym.id2name}
21
- when Symbol then send(@@#{sym})
22
- when Proc then @@#{sym}.call(self)
23
- when String then @@#{sym}
24
- else nil
25
- end
26
- end
27
- EOS
28
- end
29
- end
30
-
31
- def cattr_writer(*syms)
32
- syms.each do |sym|
33
- class_eval <<-EOS
34
- if ! defined? @@#{sym.id2name}
35
- @@#{sym.id2name} = nil
36
- end
37
-
38
- def self.#{sym.id2name}=(obj)
39
- @@#{sym.id2name} = obj
40
- end
41
-
42
- def self.set_#{sym.id2name}(obj)
43
- @@#{sym.id2name} = obj
44
- end
45
-
46
- def #{sym.id2name}=(obj)
47
- @@#{sym} = obj
48
- end
49
- EOS
50
- end
51
- end
52
-
53
- def cattr_accessor(*syms)
54
- cattr_reader(*syms)
55
- cattr_writer(*syms)
56
- end
57
- end