samoli-money 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2005 Tobias Lutke
2
+ Copyright (c) 2008 Phusion
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
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
+ ruby "-S spec -f s -c test/*_spec.rb"
18
+ end
data/lib/money.rb ADDED
@@ -0,0 +1,26 @@
1
+ # Copyright (c) 2005 Tobias Luetke
2
+ # Copyright (c) 2008 Phusion
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__))
24
+ require 'money/money'
25
+ require 'money/symbols'
26
+ require 'money/core_extensions'
@@ -0,0 +1,138 @@
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>
7
+ def to_money
8
+ Money.new(self * 100)
9
+ end
10
+ end
11
+
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">
21
+ # 'hello 2000 world'.to_money # => #<Money @cents=200000 @currency="USD")>
22
+ def to_money
23
+ # Get the currency.
24
+ matches = scan /([A-Z]{2,3})/
25
+ currency = matches[0] ? matches[0][0] : Money.default_currency
26
+ cents = calculate_cents(self)
27
+ Money.new(cents, currency)
28
+ end
29
+
30
+ private
31
+
32
+ def calculate_cents(number)
33
+ # remove anything that's not a number, potential delimiter, or minus sign
34
+ num = number.gsub(/[^\d|\.|,|\'|\s|\-]/, '').strip
35
+
36
+ # set a boolean flag for if the number is negative or not
37
+ negative = num.split(//).first == "-"
38
+
39
+ # if negative, remove the minus sign from the number
40
+ num = num.gsub(/^-/, '') if negative
41
+
42
+ # gather all separators within the result number
43
+ used_separators = num.scan /[^\d]/
44
+
45
+ # determine the number of unique separators within the number
46
+ #
47
+ # e.g.
48
+ # $1,234,567.89 would return 2 (, and .)
49
+ # $125,00 would return 1
50
+ # $199 would return 0
51
+ # $1 234,567.89 would raise an error (separators are space, comma, and period)
52
+ case used_separators.uniq.length
53
+ # no separator or delimiter; major (dollars) is the number, and minor (cents) is 0
54
+ when 0 then major, minor = num, 0
55
+
56
+ # two separators, so we know the last item in this array is the
57
+ # major/minor delimiter and the rest are separators
58
+ when 2
59
+ separator, delimiter = used_separators.uniq
60
+ # remove all separators, split on the delimiter
61
+ major, minor = num.gsub(separator, '').split(delimiter)
62
+ min = 0 unless min
63
+ when 1
64
+ # we can't determine if the comma or period is supposed to be a separator or a delimiter
65
+ # e.g.
66
+ # 1,00 - comma is a delimiter
67
+ # 1.000 - period is a delimiter
68
+ # 1,000 - comma is a separator
69
+ # 1,000,000 - comma is a separator
70
+ # 10000,00 - comma is a delimiter
71
+ # 1000,000 - comma is a delimiter
72
+
73
+ # assign first separator for reusability
74
+ separator = used_separators.first
75
+
76
+ # separator is used as a separator when there are multiple instances, always
77
+ if num.scan(separator).length > 1 # multiple matches; treat as separator
78
+ major, minor = num.gsub(separator, ''), 0
79
+ else
80
+ # ex: 1,000 - 1.0000 - 10001.000
81
+ # split number into possible major (dollars) and minor (cents) values
82
+ possible_major, possible_minor = num.split(separator)
83
+ possible_major ||= "0"
84
+ possible_minor ||= "00"
85
+
86
+ # if the minor (cents) length isn't 3, assign major/minor from the possibles
87
+ # e.g.
88
+ # 1,00 => 1.00
89
+ # 1.0000 => 1.00
90
+ # 1.2 => 1.20
91
+ if possible_minor.length != 3 # delimiter
92
+ major, minor = possible_major, possible_minor
93
+ else
94
+ # minor length is three
95
+ # let's try to figure out intent of the delimiter
96
+
97
+ # the major length is greater than three, which means
98
+ # the comma or period is used as a delimiter
99
+ # e.g.
100
+ # 1000,000
101
+ # 100000,000
102
+ if possible_major.length > 3
103
+ major, minor = possible_major, possible_minor
104
+ else
105
+ # number is in format ###{sep}### or ##{sep}### or #{sep}###
106
+ # handle as , is sep, . is delimiter
107
+ if separator == '.'
108
+ major, minor = possible_major, possible_minor
109
+ else
110
+ major, minor = "#{possible_major}#{possible_minor}", 0
111
+ end
112
+ end
113
+ end
114
+ end
115
+ else
116
+ raise ArgumentError, "Invalid currency amount"
117
+ end
118
+
119
+ # build the string based on major/minor since separator/delimiters have been removed
120
+ # avoiding floating point arithmetic here to ensure accuracy
121
+ cents = (major.to_i * 100)
122
+ # add the minor number as well. this may have any number of digits,
123
+ # so we treat minor as a string and truncate or right-fill it with zeroes
124
+ # until it becomes a two-digit number string, which we add to cents.
125
+ minor = minor.to_s
126
+ truncated_minor = minor[0..1]
127
+ truncated_minor << "0" * (2 - truncated_minor.size) if truncated_minor.size < 2
128
+ cents += truncated_minor.to_i
129
+ # respect rounding rules
130
+ if minor.size >= 3 && minor[2..2].to_i >= 5
131
+ cents += 1
132
+ end
133
+
134
+ # if negative, multiply by -1; otherwise, return positive cents
135
+ negative ? cents * -1 : cents
136
+ end
137
+
138
+ end
@@ -0,0 +1,4 @@
1
+ class Money
2
+ class UnknownRate < StandardError
3
+ end
4
+ end
@@ -0,0 +1,286 @@
1
+ require 'money/variable_exchange_bank'
2
+
3
+ # Represents an amount of money in a certain currency.
4
+ class Money
5
+ include Comparable
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)
47
+ end
48
+
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
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
+
69
+ # Creates a new money object.
70
+ # Money.new(100)
71
+ #
72
+ # Alternativly you can use the convinience methods like
73
+ # Money.ca_dollar and Money.us_dollar
74
+ def initialize(cents, currency = Money.default_currency, bank = Money.default_bank)
75
+ @cents = cents.round
76
+ @currency = currency
77
+ @bank = bank
78
+ end
79
+
80
+ # Do two money objects equal? Only works if both objects are of the same currency
81
+ def ==(other_money)
82
+ cents == other_money.cents && bank.same_currency?(currency, other_money.currency)
83
+ end
84
+
85
+ def <=>(other_money)
86
+ if bank.same_currency?(currency, other_money.currency)
87
+ cents <=> other_money.cents
88
+ else
89
+ cents <=> other_money.exchange_to(currency).cents
90
+ end
91
+ end
92
+
93
+ def +(other_money)
94
+ if currency == other_money.currency
95
+ Money.new(cents + other_money.cents, other_money.currency)
96
+ else
97
+ Money.new(cents + other_money.exchange_to(currency).cents,currency)
98
+ end
99
+ end
100
+
101
+ def -(other_money)
102
+ if currency == other_money.currency
103
+ Money.new(cents - other_money.cents, other_money.currency)
104
+ else
105
+ Money.new(cents - other_money.exchange_to(currency).cents, currency)
106
+ end
107
+ end
108
+
109
+ # get the cents value of the object
110
+ def cents
111
+ @cents
112
+ end
113
+
114
+ # multiply money by fixnum
115
+ def *(fixnum)
116
+ Money.new(cents * fixnum, currency)
117
+ end
118
+
119
+ # divide money by fixnum
120
+ def /(fixnum)
121
+ Money.new(cents / fixnum, currency)
122
+ end
123
+
124
+ # Test if the money amount is zero
125
+ def zero?
126
+ cents == 0
127
+ end
128
+
129
+
130
+ # Creates a formatted price string according to several rules. The following
131
+ # options are supported: :display_free, :with_currency, :no_cents, :symbol
132
+ # and :html.
133
+ #
134
+ # === +:display_free+
135
+ #
136
+ # Whether a zero amount of money should be formatted of "free" or as the
137
+ # supplied string.
138
+ #
139
+ # Money.us_dollar(0).format(:display_free => true) => "free"
140
+ # Money.us_dollar(0).format(:display_free => "gratis") => "gratis"
141
+ # Money.us_dollar(0).format => "$0.00"
142
+ #
143
+ # === +:with_currency+
144
+ #
145
+ # Whether the currency name should be appended to the result string.
146
+ #
147
+ # Money.ca_dollar(100).format => "$1.00"
148
+ # Money.ca_dollar(100).format(:with_currency => true) => "$1.00 CAD"
149
+ # Money.us_dollar(85).format(:with_currency => true) => "$0.85 USD"
150
+ #
151
+ # === +:no_cents+
152
+ #
153
+ # Whether cents should be omitted.
154
+ #
155
+ # Money.ca_dollar(100).format(:no_cents => true) => "$1"
156
+ # Money.ca_dollar(599).format(:no_cents => true) => "$5"
157
+ #
158
+ # Money.ca_dollar(570).format(:no_cents => true, :with_currency => true) => "$5 CAD"
159
+ # Money.ca_dollar(39000).format(:no_cents => true) => "$390"
160
+ #
161
+ # === +:symbol+
162
+ #
163
+ # Whether a money symbol should be prepended to the result string. The default is true.
164
+ # This method attempts to pick a symbol that's suitable for the given currency.
165
+ #
166
+ # Money.new(100, "USD") => "$1.00"
167
+ # Money.new(100, "GBP") => "£1.00"
168
+ # Money.new(100, "EUR") => "€1.00"
169
+ #
170
+ # # Same thing.
171
+ # Money.new(100, "USD").format(:symbol => true) => "$1.00"
172
+ # Money.new(100, "GBP").format(:symbol => true) => "£1.00"
173
+ # Money.new(100, "EUR").format(:symbol => true) => "€1.00"
174
+ #
175
+ # You can specify a false expression or an empty string to disable prepending
176
+ # a money symbol:
177
+ #
178
+ # Money.new(100, "USD").format(:symbol => false) => "1.00"
179
+ # Money.new(100, "GBP").format(:symbol => nil) => "1.00"
180
+ # Money.new(100, "EUR").format(:symbol => "") => "1.00"
181
+ #
182
+ #
183
+ # If the symbol for the given currency isn't known, then it will default
184
+ # to "$" as symbol:
185
+ #
186
+ # Money.new(100, "AWG").format(:symbol => true) => "$1.00"
187
+ #
188
+ # You can specify a string as value to enforce using a particular symbol:
189
+ #
190
+ # Money.new(100, "AWG").format(:symbol => "ƒ") => "ƒ1.00"
191
+ #
192
+ # === +:html+
193
+ #
194
+ # Whether the currency should be HTML-formatted. Only useful in combination with +:with_currency+.
195
+ #
196
+ # Money.ca_dollar(570).format(:html => true, :with_currency => true)
197
+ # => "$5.70 <span class=\"currency\">CAD</span>"
198
+ def format(*rules)
199
+ # support for old format parameters
200
+ rules = normalize_formatting_rules(rules)
201
+ rules[:symbol] = true unless rules.has_key?(:symbol)
202
+
203
+ if cents == 0
204
+ if rules[:display_free].respond_to?(:to_str)
205
+ return rules[:display_free]
206
+ elsif rules[:display_free]
207
+ return "free"
208
+ end
209
+ end
210
+
211
+ if rules[:symbol] === true
212
+ symbol = SYMBOLS[currency] || "$"
213
+ elsif rules[:symbol]
214
+ symbol = rules[:symbol]
215
+ else
216
+ symbol = ""
217
+ end
218
+
219
+ if rules[:no_cents]
220
+ formatted = sprintf("#{symbol}%d", cents.to_f / 100)
221
+ else
222
+ formatted = sprintf("#{symbol}%.2f", cents.to_f / 100)
223
+ end
224
+
225
+ # Commify ("10000" => "10,000")
226
+ formatted.gsub!(/(\d)(?=\d{3}+(?:\.|$))(\d{3}\..*)?/,'\1,\2')
227
+
228
+ if rules[:with_currency]
229
+ formatted << " "
230
+ formatted << '<span class="currency">' if rules[:html]
231
+ formatted << currency
232
+ formatted << '</span>' if rules[:html]
233
+ end
234
+ formatted
235
+ end
236
+
237
+ # Returns the amount of money as a string.
238
+ #
239
+ # Money.ca_dollar(100).to_s => "1.00"
240
+ def to_s
241
+ sprintf("%.2f", cents / 100.00)
242
+ end
243
+
244
+ # Recieve the amount of this money object in another currency.
245
+ def exchange_to(other_currency)
246
+ Money.new(@bank.exchange(self.cents, currency, other_currency), other_currency)
247
+ end
248
+
249
+ # Recieve a money object with the same amount as the current Money object
250
+ # in american dollar
251
+ def as_us_dollar
252
+ exchange_to("USD")
253
+ end
254
+
255
+ # Recieve a money object with the same amount as the current Money object
256
+ # in canadian dollar
257
+ def as_ca_dollar
258
+ exchange_to("CAD")
259
+ end
260
+
261
+ # Recieve a money object with the same amount as the current Money object
262
+ # in euro
263
+ def as_euro
264
+ exchange_to("EUR")
265
+ end
266
+
267
+ # Conversation to self
268
+ def to_money
269
+ self
270
+ end
271
+
272
+ private
273
+
274
+ def normalize_formatting_rules(rules)
275
+ if rules.size == 1
276
+ rules = rules.pop
277
+ rules = { rules => true } if rules.is_a?(Symbol)
278
+ else
279
+ rules = rules.inject({}) do |h,s|
280
+ h[s] = true
281
+ h
282
+ end
283
+ end
284
+ rules
285
+ end
286
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ # Add more from http://www.xe.com/symbols.php
4
+ Money::SYMBOLS = {
5
+ "GBP" => "£",
6
+ "JPY" => "¥",
7
+ "EUR" => "€",
8
+ "ZWD" => "Z$",
9
+ "CNY" => "¥",
10
+ "INR" => "₨",
11
+ "NPR" => "₨",
12
+ "SCR" => "₨",
13
+ "LKR" => "₨",
14
+ "SEK" => "kr",
15
+ "GHC" => "¢"
16
+
17
+ # Everything else defaults to $
18
+ }
@@ -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,24 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "samoli-money"
3
+ s.version = "2.1.4"
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 = "samoli-money"
10
+ s.authors = ["Tobias Luetke", "Hongli Lai", "Jeremy McNevin"]
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/symbols.rb",
19
+ "lib/money/variable_exchange_bank.rb",
20
+ "test/core_extensions_spec.rb",
21
+ "test/exchange_bank_spec.rb",
22
+ "test/money_spec.rb"
23
+ ]
24
+ end
@@ -0,0 +1,73 @@
1
+ $LOAD_PATH.unshift(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
+ "20.15".to_money.should == Money.new(20_15)
17
+ "100".to_money.should == Money.new(100_00)
18
+ "100.37".to_money.should == Money.new(100_37)
19
+ "100,37".to_money.should == Money.new(100_37)
20
+ "100 000".to_money.should == Money.new(100_000_00)
21
+ "100,000.00".to_money.should == Money.new(100_000_00)
22
+ "1,000".to_money.should == Money.new(1_000_00)
23
+ "-1,000".to_money.should == Money.new(-1_000_00)
24
+ "1,000.5".to_money.should == Money.new(1_000_50)
25
+ "1,000.51".to_money.should == Money.new(1_000_51)
26
+ "1,000.505".to_money.should == Money.new(1_000_51)
27
+ "1,000.504".to_money.should == Money.new(1_000_50)
28
+ "1,000.0000".to_money.should == Money.new(1_000_00)
29
+ "1,000.5000".to_money.should == Money.new(1_000_50)
30
+ "1,000.5099".to_money.should == Money.new(1_000_51)
31
+ "1.550".to_money.should == Money.new(1_55)
32
+ "25.".to_money.should == Money.new(25_00)
33
+ ".75".to_money.should == Money.new(75)
34
+
35
+ "100 USD".to_money.should == Money.new(100_00, "USD")
36
+ "-100 USD".to_money.should == Money.new(-100_00, "USD")
37
+ "100 EUR".to_money.should == Money.new(100_00, "EUR")
38
+ "100.37 EUR".to_money.should == Money.new(100_37, "EUR")
39
+ "100,37 EUR".to_money.should == Money.new(100_37, "EUR")
40
+ "100,000.00 USD".to_money.should == Money.new(100_000_00, "USD")
41
+ "100.000,00 EUR".to_money.should == Money.new(100_000_00, "EUR")
42
+ "1,000 USD".to_money.should == Money.new(1_000_00, "USD")
43
+ "-1,000 USD".to_money.should == Money.new(-1_000_00, "USD")
44
+ "1,000.5500 USD".to_money.should == Money.new(1_000_55, "USD")
45
+ "-1,000.6500 USD".to_money.should == Money.new(-1_000_65, "USD")
46
+ "1.550 USD".to_money.should == Money.new(1_55, "USD")
47
+
48
+ "USD 100".to_money.should == Money.new(100_00, "USD")
49
+ "EUR 100".to_money.should == Money.new(100_00, "EUR")
50
+ "EUR 100.37".to_money.should == Money.new(100_37, "EUR")
51
+ "CAD -100.37".to_money.should == Money.new(-100_37, "CAD")
52
+ "EUR 100,37".to_money.should == Money.new(100_37, "EUR")
53
+ "EUR -100,37".to_money.should == Money.new(-100_37, "EUR")
54
+ "USD 100,000.00".to_money.should == Money.new(100_000_00, "USD")
55
+ "EUR 100.000,00".to_money.should == Money.new(100_000_00, "EUR")
56
+ "USD 1,000".to_money.should == Money.new(1_000_00, "USD")
57
+ "USD -1,000".to_money.should == Money.new(-1_000_00, "USD")
58
+ "USD 1,000.9000".to_money.should == Money.new(1_000_90, "USD")
59
+ "USD -1,000.090".to_money.should == Money.new(-1_000_09, "USD")
60
+ "USD 1.5500".to_money.should == Money.new(1_55, "USD")
61
+
62
+ "$100 USD".to_money.should == Money.new(100_00, "USD")
63
+ "$1,194.59 USD".to_money.should == Money.new(1_194_59, "USD")
64
+ "$-1,955 USD".to_money.should == Money.new(-1_955_00, "USD")
65
+ "$1,194.5900 USD".to_money.should == Money.new(1_194_59, "USD")
66
+ "$-1,955.000 USD".to_money.should == Money.new(-1_955_00, "USD")
67
+ "$1.99000 USD".to_money.should == Money.new(1_99, "USD")
68
+ end
69
+
70
+ specify "String#to_money ignores unrecognized data" do
71
+ "hello 2000 world".to_money.should == Money.new(2000_00)
72
+ end
73
+ end
@@ -0,0 +1,45 @@
1
+ $LOAD_PATH.unshift(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,244 @@
1
+ # encoding: utf-8
2
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
3
+ require 'money/money'
4
+ require 'money/symbols'
5
+
6
+ describe Money do
7
+ it "is associated to the singleton instance of VariableExchangeBank by default" do
8
+ Money.new(0).bank.object_id.should == Money::VariableExchangeBank.instance.object_id
9
+ end
10
+
11
+ specify "#cents returns the amount of cents passed to the constructor" do
12
+ Money.new(200_00, "USD").cents.should == 200_00
13
+ end
14
+
15
+ it "rounds the given cents to an integer" do
16
+ Money.new(1.00, "USD").cents.should == 1
17
+ Money.new(1.01, "USD").cents.should == 1
18
+ Money.new(1.50, "USD").cents.should == 2
19
+ end
20
+
21
+ specify "#currency returns the currency passed to the constructor" do
22
+ Money.new(200_00, "USD").currency.should == "USD"
23
+ end
24
+
25
+ specify "#zero? returns whether the amount is 0" do
26
+ Money.new(0, "USD").should be_zero
27
+ Money.new(0, "EUR").should be_zero
28
+ Money.new(1, "USD").should_not be_zero
29
+ Money.new(10, "YEN").should_not be_zero
30
+ Money.new(-1, "EUR").should_not be_zero
31
+ end
32
+
33
+ specify "#exchange_to exchanges the amount via its exchange bank" do
34
+ money = Money.new(100_00, "USD")
35
+ money.bank.should_receive(:exchange).with(100_00, "USD", "EUR").and_return(200_00)
36
+ money.exchange_to("EUR")
37
+ end
38
+
39
+ specify "#exchange_to exchanges the amount properly" do
40
+ money = Money.new(100_00, "USD")
41
+ money.bank.should_receive(:exchange).with(100_00, "USD", "EUR").and_return(200_00)
42
+ money.exchange_to("EUR").should == Money.new(200_00, "EUR")
43
+ end
44
+
45
+ specify "#== returns true if and only if their amount and currency are equal" do
46
+ Money.new(1_00, "USD").should == Money.new(1_00, "USD")
47
+ Money.new(1_00, "USD").should_not == Money.new(1_00, "EUR")
48
+ Money.new(1_00, "USD").should_not == Money.new(2_00, "USD")
49
+ Money.new(1_00, "USD").should_not == Money.new(99_00, "EUR")
50
+ end
51
+
52
+ specify "#* multiplies the money's amount by the multiplier while retaining the currency" do
53
+ (Money.new(1_00, "USD") * 10).should == Money.new(10_00, "USD")
54
+ end
55
+
56
+ specify "#* divides the money's amount by the divisor while retaining the currency" do
57
+ (Money.new(10_00, "USD") / 10).should == Money.new(1_00, "USD")
58
+ end
59
+
60
+ specify "Money.empty creates a new Money object of 0 cents" do
61
+ Money.empty.should == Money.new(0)
62
+ end
63
+
64
+ specify "Money.ca_dollar creates a new Money object of the given value in CAD" do
65
+ Money.ca_dollar(50).should == Money.new(50, "CAD")
66
+ end
67
+
68
+ specify "Money.ca_dollar creates a new Money object of the given value in USD" do
69
+ Money.us_dollar(50).should == Money.new(50, "USD")
70
+ end
71
+
72
+ specify "Money.ca_dollar creates a new Money object of the given value in EUR" do
73
+ Money.euro(50).should == Money.new(50, "EUR")
74
+ end
75
+
76
+ specify "Money.add_rate works" do
77
+ Money.add_rate("EUR", "USD", 10)
78
+ Money.new(10_00, "EUR").exchange_to("USD").should == Money.new(100_00, "USD")
79
+ end
80
+
81
+ describe "#format" do
82
+ it "returns the monetary value as a string" do
83
+ Money.ca_dollar(100).format.should == "$1.00"
84
+ end
85
+
86
+ describe "if the monetary value is 0" do
87
+ before :each do
88
+ @money = Money.us_dollar(0)
89
+ end
90
+
91
+ it "returns 'free' when :display_free is true" do
92
+ @money.format(:display_free => true).should == 'free'
93
+ end
94
+
95
+ it "returns '$0.00' when :display_free is false or not given" do
96
+ @money.format.should == '$0.00'
97
+ @money.format(:display_free => false).should == '$0.00'
98
+ @money.format(:display_free => nil).should == '$0.00'
99
+ end
100
+
101
+ it "returns the value specified by :display_free if it's a string-like object" do
102
+ @money.format(:display_free => 'gratis').should == 'gratis'
103
+ end
104
+ end
105
+
106
+ specify "#format(:with_currency => true) works as documented" do
107
+ Money.ca_dollar(100).format(:with_currency => true).should == "$1.00 CAD"
108
+ Money.us_dollar(85).format(:with_currency => true).should == "$0.85 USD"
109
+ Money.us_dollar(85).format(:with_currency).should == "$0.85 USD"
110
+ end
111
+
112
+ specify "#format(:with_currency) works as documented" do
113
+ Money.ca_dollar(100).format(:with_currency).should == "$1.00 CAD"
114
+ Money.us_dollar(85).format(:with_currency).should == "$0.85 USD"
115
+ end
116
+
117
+ specify "#format(:no_cents => true) works as documented" do
118
+ Money.ca_dollar(100).format(:no_cents => true).should == "$1"
119
+ Money.ca_dollar(599).format(:no_cents => true).should == "$5"
120
+ Money.ca_dollar(570).format(:no_cents => true, :with_currency => true).should == "$5 CAD"
121
+ Money.ca_dollar(39000).format(:no_cents => true).should == "$390"
122
+ end
123
+
124
+ specify "#format(:no_cents) works as documented" do
125
+ Money.ca_dollar(100).format(:no_cents).should == "$1"
126
+ Money.ca_dollar(599).format(:no_cents).should == "$5"
127
+ Money.ca_dollar(570).format(:no_cents, :with_currency).should == "$5 CAD"
128
+ Money.ca_dollar(39000).format(:no_cents).should == "$390"
129
+ end
130
+
131
+ specify "#format(:symbol => a symbol string) uses the given value as the money symbol" do
132
+ Money.new(100, :currency => "GBP").format(:symbol => "£").should == "£1.00"
133
+ end
134
+
135
+ specify "#format(:symbol => true) returns symbol based on the given currency code" do
136
+ one = Proc.new {|currency| Money.new(100, currency).format(:symbol => true) }
137
+
138
+ # Pounds
139
+ one["GBP"].should == "£1.00"
140
+
141
+ # Dollars
142
+ one["USD"].should == "$1.00"
143
+ one["CAD"].should == "$1.00"
144
+ one["AUD"].should == "$1.00"
145
+ one["NZD"].should == "$1.00"
146
+ one["ZWD"].should == "Z$1.00"
147
+
148
+ # Yen
149
+ one["JPY"].should == "¥1.00"
150
+ one["CNY"].should == "¥1.00"
151
+
152
+ # Euro
153
+ one["EUR"].should == "€1.00"
154
+
155
+ # Rupees
156
+ one["INR"].should == "₨1.00"
157
+ one["NPR"].should == "₨1.00"
158
+ one["SCR"].should == "₨1.00"
159
+ one["LKR"].should == "₨1.00"
160
+
161
+ # Other
162
+ one["SEK"].should == "kr1.00"
163
+ one["GHC"].should == "¢1.00"
164
+ end
165
+
166
+ specify "#format(:symbol => true) returns $ when currency code is not recognized" do
167
+ Money.new(100, :currency => "XYZ").format(:symbol => true).should == "$1.00"
168
+ end
169
+
170
+ specify "#format(:symbol => some non-Boolean value that evaluates to true) returs symbol based on the given currency code" do
171
+ Money.new(100, "GBP").format(:symbol => true).should == "£1.00"
172
+ Money.new(100, "EUR").format(:symbol => true).should == "€1.00"
173
+ Money.new(100, "SEK").format(:symbol => true).should == "kr1.00"
174
+ end
175
+
176
+ specify "#format with :symbol == "", nil or false returns the amount without a symbol" do
177
+ money = Money.new(100, "GBP")
178
+ money.format(:symbol => "").should == "1.00"
179
+ money.format(:symbol => nil).should == "1.00"
180
+ money.format(:symbol => false).should == "1.00"
181
+ end
182
+
183
+ specify "#format without :symbol key set works as documented" do
184
+ money = Money.new(100, "GBP")
185
+ money.format.should == "£1.00"
186
+ end
187
+
188
+ specify "#format(:html => true) works as documented" do
189
+ string = Money.ca_dollar(570).format(:html => true, :with_currency => true)
190
+ string.should == "$5.70 <span class=\"currency\">CAD</span>"
191
+ end
192
+
193
+ it "should insert commas into the result if the amount is sufficiently large" do
194
+ Money.us_dollar(1_000_000_000_12).format.should == "$1,000,000,000.12"
195
+ Money.us_dollar(1_000_000_000_12).format(:no_cents => true).should == "$1,000,000,000"
196
+ end
197
+ end
198
+ end
199
+
200
+ describe "Actions involving two Money objects" do
201
+ describe "if the other Money object has the same currency" do
202
+ specify "#<=> compares the two objects' amounts" do
203
+ (Money.new(1_00, "USD") <=> Money.new(1_00, "USD")).should == 0
204
+ (Money.new(1_00, "USD") <=> Money.new(99, "USD")).should > 0
205
+ (Money.new(1_00, "USD") <=> Money.new(2_00, "USD")).should < 0
206
+ end
207
+
208
+ specify "#+ adds the other object's amount to the current object's amount while retaining the currency" do
209
+ (Money.new(10_00, "USD") + Money.new(90, "USD")).should == Money.new(10_90, "USD")
210
+ end
211
+
212
+ specify "#- substracts the other object's amount from the current object's amount while retaining the currency" do
213
+ (Money.new(10_00, "USD") - Money.new(90, "USD")).should == Money.new(9_10, "USD")
214
+ end
215
+ end
216
+
217
+ describe "if the other Money object has a different currency" do
218
+ specify "#<=> compares the two objects' amount after converting the other object's amount to its own currency" do
219
+ target = Money.new(200_00, "EUR")
220
+ target.should_receive(:exchange_to).with("USD").and_return(Money.new(300_00, "USD"))
221
+ (Money.new(100_00, "USD") <=> target).should < 0
222
+
223
+ target = Money.new(200_00, "EUR")
224
+ target.should_receive(:exchange_to).with("USD").and_return(Money.new(100_00, "USD"))
225
+ (Money.new(100_00, "USD") <=> target).should == 0
226
+
227
+ target = Money.new(200_00, "EUR")
228
+ target.should_receive(:exchange_to).with("USD").and_return(Money.new(99_00, "USD"))
229
+ (Money.new(100_00, "USD") <=> target).should > 0
230
+ end
231
+
232
+ specify "#+ adds the other object's amount, converted to this object's currency, to this object's amount while retaining its currency" do
233
+ other = Money.new(90, "EUR")
234
+ other.should_receive(:exchange_to).with("USD").and_return(Money.new(9_00, "USD"))
235
+ (Money.new(10_00, "USD") + other).should == Money.new(19_00, "USD")
236
+ end
237
+
238
+ specify "#- substracts the other object's amount, converted to this object's currency, from this object's amount while retaining its currency" do
239
+ other = Money.new(90, "EUR")
240
+ other.should_receive(:exchange_to).with("USD").and_return(Money.new(9_00, "USD"))
241
+ (Money.new(10_00, "USD") - other).should == Money.new(1_00, "USD")
242
+ end
243
+ end
244
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: samoli-money
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.1.4
5
+ platform: ruby
6
+ authors:
7
+ - Tobias Luetke
8
+ - Hongli Lai
9
+ - Jeremy McNevin
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2009-11-04 00:00:00 +00:00
15
+ default_executable:
16
+ dependencies: []
17
+
18
+ description: Money and currency exchange support library.
19
+ email: hongli@phusion.nl
20
+ executables: []
21
+
22
+ extensions: []
23
+
24
+ extra_rdoc_files: []
25
+
26
+ files:
27
+ - README.rdoc
28
+ - MIT-LICENSE
29
+ - money.gemspec
30
+ - Rakefile
31
+ - lib/money.rb
32
+ - lib/money/core_extensions.rb
33
+ - lib/money/errors.rb
34
+ - lib/money/money.rb
35
+ - lib/money/symbols.rb
36
+ - lib/money/variable_exchange_bank.rb
37
+ - test/core_extensions_spec.rb
38
+ - test/exchange_bank_spec.rb
39
+ - test/money_spec.rb
40
+ has_rdoc: true
41
+ homepage: http://money.rubyforge.org/
42
+ licenses: []
43
+
44
+ post_install_message:
45
+ rdoc_options: []
46
+
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project: samoli-money
64
+ rubygems_version: 1.3.5
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: Money and currency exchange support library
68
+ test_files: []
69
+