money 3.6.1 → 3.6.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,350 @@
1
+ class Money
2
+ module Parsing
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ # Parses the current string and converts it to a +Money+ object.
9
+ # Excess characters will be discarded.
10
+ #
11
+ # @param [String, #to_s] input The input to parse.
12
+ # @param [Currency, String, Symbol] currency The currency format.
13
+ # The currency to set the resulting +Money+ object to.
14
+ #
15
+ # @return [Money]
16
+ #
17
+ # @raise [ArgumentError] If any +currency+ is supplied and
18
+ # given value doesn't match the one extracted from
19
+ # the +input+ string.
20
+ #
21
+ # @example
22
+ # '100'.to_money #=> #<Money @cents=10000>
23
+ # '100.37'.to_money #=> #<Money @cents=10037>
24
+ # '100 USD'.to_money #=> #<Money @cents=10000, @currency=#<Money::Currency id: usd>>
25
+ # 'USD 100'.to_money #=> #<Money @cents=10000, @currency=#<Money::Currency id: usd>>
26
+ # '$100 USD'.to_money #=> #<Money @cents=10000, @currency=#<Money::Currency id: usd>>
27
+ # 'hello 2000 world'.to_money #=> #<Money @cents=200000 @currency=#<Money::Currency id: usd>>
28
+ #
29
+ # @example Mismatching currencies
30
+ # 'USD 2000'.to_money("EUR") #=> ArgumentError
31
+ #
32
+ # @see Money.from_string
33
+ #
34
+ def parse(input, currency = nil)
35
+ i = input.to_s
36
+
37
+ # Get the currency.
38
+ m = i.scan /([A-Z]{2,3})/
39
+ c = m[0] ? m[0][0] : nil
40
+
41
+ # check that currency passed and embedded currency are the same,
42
+ # and negotiate the final currency
43
+ if currency.nil? and c.nil?
44
+ currency = Money.default_currency
45
+ elsif currency.nil?
46
+ currency = c
47
+ elsif c.nil?
48
+ currency = currency
49
+ elsif currency != c
50
+ # TODO: ParseError
51
+ raise ArgumentError, "Mismatching Currencies"
52
+ end
53
+ currency = Money::Currency.wrap(currency)
54
+
55
+ cents = extract_cents(i, currency)
56
+ new(cents, currency)
57
+ end
58
+
59
+ # Converts a String into a Money object treating the +value+
60
+ # as dollars and converting them to the corresponding cents value,
61
+ # according to +currency+ subunit property,
62
+ # before instantiating the Money object.
63
+ #
64
+ # Behind the scenes, this method relies on {Money.from_bigdecimal}
65
+ # to avoid problems with string-to-numeric conversion.
66
+ #
67
+ # @param [String, #to_s] value The money amount, in dollars.
68
+ # @param [Currency, String, Symbol] currency
69
+ # The currency to set the resulting +Money+ object to.
70
+ #
71
+ # @return [Money]
72
+ #
73
+ # @example
74
+ # Money.from_string("100")
75
+ # #=> #<Money @cents=10000 @currency="USD">
76
+ # Money.from_string("100", "USD")
77
+ # #=> #<Money @cents=10000 @currency="USD">
78
+ # Money.from_string("100", "EUR")
79
+ # #=> #<Money @cents=10000 @currency="EUR">
80
+ # Money.from_string("100", "BHD")
81
+ # #=> #<Money @cents=100 @currency="BHD">
82
+ #
83
+ # @see String#to_money
84
+ # @see Money.parse
85
+ #
86
+ def from_string(value, currency = Money.default_currency)
87
+ from_bigdecimal(BigDecimal.new(value.to_s), currency)
88
+ end
89
+
90
+ # Converts a Fixnum into a Money object treating the +value+
91
+ # as dollars and converting them to the corresponding cents value,
92
+ # according to +currency+ subunit property,
93
+ # before instantiating the Money object.
94
+ #
95
+ # @param [Fixnum] value The money amount, in dollars.
96
+ # @param [Currency, String, Symbol] currency The currency format.
97
+ #
98
+ # @return [Money]
99
+ #
100
+ # @example
101
+ # Money.from_fixnum(100)
102
+ # #=> #<Money @cents=10000 @currency="USD">
103
+ # Money.from_fixnum(100, "USD")
104
+ # #=> #<Money @cents=10000 @currency="USD">
105
+ # Money.from_fixnum(100, "EUR")
106
+ # #=> #<Money @cents=10000 @currency="EUR">
107
+ # Money.from_fixnum(100, "BHD")
108
+ # #=> #<Money @cents=100 @currency="BHD">
109
+ #
110
+ # @see Fixnum#to_money
111
+ # @see Money.from_numeric
112
+ #
113
+ def from_fixnum(value, currency = Money.default_currency)
114
+ currency = Money::Currency.wrap(currency)
115
+ amount = value * currency.subunit_to_unit
116
+ new(amount, currency)
117
+ end
118
+
119
+ # Converts a Float into a Money object treating the +value+
120
+ # as dollars and converting them to the corresponding cents value,
121
+ # according to +currency+ subunit property,
122
+ # before instantiating the Money object.
123
+ #
124
+ # Behind the scenes, this method relies on Money.from_bigdecimal
125
+ # to avoid problems with floating point precision.
126
+ #
127
+ # @param [Float] value The money amount, in dollars.
128
+ # @param [Currency, String, Symbol] currency The currency format.
129
+ #
130
+ # @return [Money]
131
+ #
132
+ # @example
133
+ # Money.from_float(100.0)
134
+ # #=> #<Money @cents=10000 @currency="USD">
135
+ # Money.from_float(100.0, "USD")
136
+ # #=> #<Money @cents=10000 @currency="USD">
137
+ # Money.from_float(100.0, "EUR")
138
+ # #=> #<Money @cents=10000 @currency="EUR">
139
+ # Money.from_float(100.0, "BHD")
140
+ # #=> #<Money @cents=100 @currency="BHD">
141
+ #
142
+ # @see Float#to_money
143
+ # @see Money.from_numeric
144
+ #
145
+ def from_float(value, currency = Money.default_currency)
146
+ from_bigdecimal(BigDecimal.new(value.to_s), currency)
147
+ end
148
+
149
+ # Converts a BigDecimal into a Money object treating the +value+
150
+ # as dollars and converting them to the corresponding cents value,
151
+ # according to +currency+ subunit property,
152
+ # before instantiating the Money object.
153
+ #
154
+ # @param [BigDecimal] value The money amount, in dollars.
155
+ # @param [Currency, String, Symbol] currency The currency format.
156
+ #
157
+ # @return [Money]
158
+ #
159
+ # @example
160
+ # Money.from_bigdecimal(BigDecimal.new("100")
161
+ # #=> #<Money @cents=10000 @currency="USD">
162
+ # Money.from_bigdecimal(BigDecimal.new("100", "USD")
163
+ # #=> #<Money @cents=10000 @currency="USD">
164
+ # Money.from_bigdecimal(BigDecimal.new("100", "EUR")
165
+ # #=> #<Money @cents=10000 @currency="EUR">
166
+ # Money.from_bigdecimal(BigDecimal.new("100", "BHD")
167
+ # #=> #<Money @cents=100 @currency="BHD">
168
+ #
169
+ # @see BigDecimal#to_money
170
+ # @see Money.from_numeric
171
+ #
172
+ def from_bigdecimal(value, currency = Money.default_currency)
173
+ currency = Money::Currency.wrap(currency)
174
+ amount = value * currency.subunit_to_unit
175
+ new(amount.fix, currency)
176
+ end
177
+
178
+ # Converts a Numeric value into a Money object treating the +value+
179
+ # as dollars and converting them to the corresponding cents value,
180
+ # according to +currency+ subunit property,
181
+ # before instantiating the Money object.
182
+ #
183
+ # This method relies on various +Money.from_*+ methods
184
+ # and tries to forwards the call to the most appropriate method
185
+ # in order to reduce computation effort.
186
+ # For instance, if +value+ is an Integer, this method calls
187
+ # {Money.from_fixnum} instead of using the default
188
+ # {Money.from_bigdecimal} which adds the overload to converts
189
+ # the value into a slower BigDecimal instance.
190
+ #
191
+ # @param [Numeric] value The money amount, in dollars.
192
+ # @param [Currency, String, Symbol] currency The currency format.
193
+ #
194
+ # @return [Money]
195
+ #
196
+ # @raise +ArgumentError+ Unless +value+ is a supported type.
197
+ #
198
+ # @example
199
+ # Money.from_numeric(100)
200
+ # #=> #<Money @cents=10000 @currency="USD">
201
+ # Money.from_numeric(100.00)
202
+ # #=> #<Money @cents=10000 @currency="USD">
203
+ # Money.from_numeric("100")
204
+ # #=> ArgumentError
205
+ #
206
+ # @see Numeric#to_money
207
+ # @see Money.from_fixnum
208
+ # @see Money.from_float
209
+ # @see Money.from_bigdecimal
210
+ #
211
+ def from_numeric(value, currency = Money.default_currency)
212
+ case value
213
+ when Fixnum
214
+ from_fixnum(value, currency)
215
+ when Numeric
216
+ from_bigdecimal(BigDecimal.new(value.to_s), currency)
217
+ else
218
+ raise ArgumentError, "`value' should be a Numeric object"
219
+ end
220
+ end
221
+
222
+ # Takes a number string and attempts to massage out the number.
223
+ #
224
+ # @param [String] input The string containing a potential number.
225
+ #
226
+ # @return [Integer]
227
+ #
228
+ def extract_cents(input, currency = Money.default_currency)
229
+ # remove anything that's not a number, potential thousands_separator, or minus sign
230
+ num = input.gsub(/[^\d|\.|,|\'|\-]/, '').strip
231
+
232
+ # set a boolean flag for if the number is negative or not
233
+ negative = num.split(//).first == "-"
234
+
235
+ # if negative, remove the minus sign from the number
236
+ # if it's not negative, the hyphen makes the value invalid
237
+ if negative
238
+ num = num.gsub(/^-/, '')
239
+ else
240
+ raise ArgumentError, "Invalid currency amount (hyphen)" if num.include?('-')
241
+ end
242
+
243
+ #if the number ends with punctuation, just throw it out. If it means decimal,
244
+ #it won't hurt anything. If it means a literal period or comma, this will
245
+ #save it from being mis-interpreted as a decimal.
246
+ num.chop! if num.match /[\.|,]$/
247
+
248
+ # gather all decimal_marks within the result number
249
+ used_decimal_marks = num.scan /[^\d]/
250
+
251
+ # determine the number of unique decimal_marks within the number
252
+ #
253
+ # e.g.
254
+ # $1,234,567.89 would return 2 (, and .)
255
+ # $125,00 would return 1
256
+ # $199 would return 0
257
+ # $1 234,567.89 would raise an error (decimal_marks are space, comma, and period)
258
+ case used_decimal_marks.uniq.length
259
+ # no decimal_mark or thousands_separator; major (dollars) is the number, and minor (cents) is 0
260
+ when 0 then major, minor = num, 0
261
+
262
+ # two decimal_marks, so we know the last item in this array is the
263
+ # major/minor thousands_separator and the rest are decimal_marks
264
+ when 2
265
+ decimal_mark, thousands_separator = used_decimal_marks.uniq
266
+ # remove all decimal_marks, split on the thousands_separator
267
+ major, minor = num.gsub(decimal_mark, '').split(thousands_separator)
268
+ min = 0 unless min
269
+ when 1
270
+ # we can't determine if the comma or period is supposed to be a decimal_mark or a thousands_separator
271
+ # e.g.
272
+ # 1,00 - comma is a thousands_separator
273
+ # 1.000 - period is a thousands_separator
274
+ # 1,000 - comma is a decimal_mark
275
+ # 1,000,000 - comma is a decimal_mark
276
+ # 10000,00 - comma is a thousands_separator
277
+ # 1000,000 - comma is a thousands_separator
278
+
279
+ # assign first decimal_mark for reusability
280
+ decimal_mark = used_decimal_marks.first
281
+
282
+ # decimal_mark is used as a decimal_mark when there are multiple instances, always
283
+ if num.scan(decimal_mark).length > 1 # multiple matches; treat as decimal_mark
284
+ major, minor = num.gsub(decimal_mark, ''), 0
285
+ else
286
+ # ex: 1,000 - 1.0000 - 10001.000
287
+ # split number into possible major (dollars) and minor (cents) values
288
+ possible_major, possible_minor = num.split(decimal_mark)
289
+ possible_major ||= "0"
290
+ possible_minor ||= "00"
291
+
292
+ # if the minor (cents) length isn't 3, assign major/minor from the possibles
293
+ # e.g.
294
+ # 1,00 => 1.00
295
+ # 1.0000 => 1.00
296
+ # 1.2 => 1.20
297
+ if possible_minor.length != 3 # thousands_separator
298
+ major, minor = possible_major, possible_minor
299
+ else
300
+ # minor length is three
301
+ # let's try to figure out intent of the thousands_separator
302
+
303
+ # the major length is greater than three, which means
304
+ # the comma or period is used as a thousands_separator
305
+ # e.g.
306
+ # 1000,000
307
+ # 100000,000
308
+ if possible_major.length > 3
309
+ major, minor = possible_major, possible_minor
310
+ else
311
+ # number is in format ###{sep}### or ##{sep}### or #{sep}###
312
+ # handle as , is sep, . is thousands_separator
313
+ if decimal_mark == '.'
314
+ major, minor = possible_major, possible_minor
315
+ else
316
+ major, minor = "#{possible_major}#{possible_minor}", 0
317
+ end
318
+ end
319
+ end
320
+ end
321
+ else
322
+ # TODO: ParseError
323
+ raise ArgumentError, "Invalid currency amount"
324
+ end
325
+
326
+ # build the string based on major/minor since decimal_mark/thousands_separator have been removed
327
+ # avoiding floating point arithmetic here to ensure accuracy
328
+ cents = (major.to_i * currency.subunit_to_unit)
329
+ # Because of an bug in JRuby, we can't just call #floor
330
+ minor = minor.to_s
331
+ minor = if minor.size < currency.decimal_places
332
+ (minor + ("0" * currency.decimal_places))[0,currency.decimal_places].to_i
333
+ elsif minor.size > currency.decimal_places
334
+ if minor[currency.decimal_places,1].to_i >= 5
335
+ minor[0,currency.decimal_places].to_i+1
336
+ else
337
+ minor[0,currency.decimal_places].to_i
338
+ end
339
+ else
340
+ minor.to_i
341
+ end
342
+ cents += minor
343
+
344
+ # if negative, multiply by -1; otherwise, return positive cents
345
+ negative ? cents * -1 : cents
346
+ end
347
+
348
+ end
349
+ end
350
+ end
data/money.gemspec CHANGED
@@ -1,27 +1,34 @@
1
- Gem::Specification.new do |s|
2
- s.name = "money"
3
- s.version = "3.6.1"
4
- s.platform = Gem::Platform::RUBY
5
- s.authors = ["Tobias Luetke", "Hongli Lai", "Jeremy McNevin", "Shane Emmons", "Simone Carletti"]
6
- s.email = ["hongli@phusion.nl", "semmons99+RubyMoney@gmail.com"]
7
- s.homepage = "http://money.rubyforge.org"
8
- s.summary = "Money and currency exchange support library."
9
- s.description = "This library aids one in handling money and different currencies."
10
-
11
- s.required_rubygems_version = ">= 1.3.6"
12
- s.rubyforge_project = "money"
13
-
14
- s.add_dependency "i18n", "~> 0.4"
15
-
16
- s.add_development_dependency "rspec", ">= 2.0.0"
17
- s.add_development_dependency "yard"
18
- s.add_development_dependency "json"
19
-
20
- s.requirements << "json if you plan to import/export rates formatted as json"
21
-
22
- s.files = Dir.glob("{lib,spec}/**/*")
23
- s.files += %w(CHANGELOG.md LICENSE README.md)
24
- s.files += %w(Rakefile .gemtest money.gemspec)
25
-
26
- s.require_path = "lib"
27
- end
1
+ Gem::Specification.new do |s|
2
+ s.name = "money"
3
+ s.version = "3.6.2"
4
+ s.platform = Gem::Platform::RUBY
5
+ s.authors = ["Tobias Luetke", "Hongli Lai", "Jeremy McNevin",
6
+ "Shane Emmons", "Simone Carletti", "Jean-Louis Giordano",
7
+ "Joshua Clayton", "Bodaniel Jeanes", "Tobias Schmidt",
8
+ "Chris Kampmeier", "Romain Gérard", "Eloy", "Josh Delsman",
9
+ "Pelle Braendgaard", "Tom Lianza", "James Cotterill",
10
+ "François Beausoleil", "Abhay Kumar", "pconnor",
11
+ "Christian Billen", "Ilia Lobsanov",
12
+ ]
13
+ s.email = ["hongli@phusion.nl", "semmons99+RubyMoney@gmail.com"]
14
+ s.homepage = "http://money.rubyforge.org"
15
+ s.summary = "Money and currency exchange support library."
16
+ s.description = "This library aids one in handling money and different currencies."
17
+
18
+ s.required_rubygems_version = ">= 1.3.6"
19
+ s.rubyforge_project = "money"
20
+
21
+ s.add_dependency "i18n", "~> 0.4"
22
+
23
+ s.add_development_dependency "rspec", ">= 2.0.0"
24
+ s.add_development_dependency "yard"
25
+ s.add_development_dependency "json"
26
+
27
+ s.requirements << "json if you plan to import/export rates formatted as json"
28
+
29
+ s.files = Dir.glob("{lib,spec}/**/*")
30
+ s.files += %w(CHANGELOG.md LICENSE README.md)
31
+ s.files += %w(Rakefile .gemtest money.gemspec)
32
+
33
+ s.require_path = "lib"
34
+ end
@@ -1,72 +1,72 @@
1
- require "spec_helper"
2
-
3
- describe Money::Bank::Base do
4
- before :each do
5
- @bank = Money::Bank::Base.new
6
- end
7
-
8
- describe '#new with &block' do
9
- it 'should store @rounding_method' do
10
- proc = Proc.new{|n| n.ceil}
11
- bank = Money::Bank::Base.new(&proc)
12
- bank.rounding_method.should == proc
13
- end
14
- end
15
-
16
- describe '#setup' do
17
- it 'should call #setup after #initialize' do
18
- class MyBank < Money::Bank::Base
19
- attr_reader :setup_called
20
-
21
- def setup
22
- @setup_called = true
23
- end
24
- end
25
-
26
- bank = MyBank.new
27
- bank.setup_called.should == true
28
- end
29
- end
30
-
31
- describe '#exchange_with' do
32
- it 'should raise NotImplementedError' do
33
- lambda { @bank.exchange_with(Money.new(100, 'USD'), 'EUR') }.should raise_exception(NotImplementedError)
34
- end
35
- end
36
-
37
- describe '#same_currency?' do
38
- it 'should accept str/str' do
39
- lambda{@bank.send(:same_currency?, 'USD', 'EUR')}.should_not raise_exception
40
- end
41
-
42
- it 'should accept currency/str' do
43
- lambda{@bank.send(:same_currency?, Money::Currency.wrap('USD'), 'EUR')}.should_not raise_exception
44
- end
45
-
46
- it 'should accept str/currency' do
47
- lambda{@bank.send(:same_currency?, 'USD', Money::Currency.wrap('EUR'))}.should_not raise_exception
48
- end
49
-
50
- it 'should accept currency/currency' do
51
- lambda{@bank.send(:same_currency?, Money::Currency.wrap('USD'), Money::Currency.wrap('EUR'))}.should_not raise_exception
52
- end
53
-
54
- it 'should return `true` when currencies match' do
55
- @bank.send(:same_currency?, 'USD', 'USD').should == true
56
- @bank.send(:same_currency?, Money::Currency.wrap('USD'), 'USD').should == true
57
- @bank.send(:same_currency?, 'USD', Money::Currency.wrap('USD')).should == true
58
- @bank.send(:same_currency?, Money::Currency.wrap('USD'), Money::Currency.wrap('USD')).should == true
59
- end
60
-
61
- it 'should return `false` when currencies do not match' do
62
- @bank.send(:same_currency?, 'USD', 'EUR').should == false
63
- @bank.send(:same_currency?, Money::Currency.wrap('USD'), 'EUR').should == false
64
- @bank.send(:same_currency?, 'USD', Money::Currency.wrap('EUR')).should == false
65
- @bank.send(:same_currency?, Money::Currency.wrap('USD'), Money::Currency.wrap('EUR')).should == false
66
- end
67
-
68
- it 'should raise an UnknownCurrency exception when an unknown currency is passed' do
69
- lambda{@bank.send(:same_currency?, 'AAA', 'BBB')}.should raise_exception(Money::Currency::UnknownCurrency)
70
- end
71
- end
72
- end
1
+ require "spec_helper"
2
+
3
+ describe Money::Bank::Base do
4
+ before :each do
5
+ @bank = Money::Bank::Base.new
6
+ end
7
+
8
+ describe '#new with &block' do
9
+ it 'should store @rounding_method' do
10
+ proc = Proc.new{|n| n.ceil}
11
+ bank = Money::Bank::Base.new(&proc)
12
+ bank.rounding_method.should == proc
13
+ end
14
+ end
15
+
16
+ describe '#setup' do
17
+ it 'should call #setup after #initialize' do
18
+ class MyBank < Money::Bank::Base
19
+ attr_reader :setup_called
20
+
21
+ def setup
22
+ @setup_called = true
23
+ end
24
+ end
25
+
26
+ bank = MyBank.new
27
+ bank.setup_called.should == true
28
+ end
29
+ end
30
+
31
+ describe '#exchange_with' do
32
+ it 'should raise NotImplementedError' do
33
+ lambda { @bank.exchange_with(Money.new(100, 'USD'), 'EUR') }.should raise_exception(NotImplementedError)
34
+ end
35
+ end
36
+
37
+ describe '#same_currency?' do
38
+ it 'should accept str/str' do
39
+ lambda{@bank.send(:same_currency?, 'USD', 'EUR')}.should_not raise_exception
40
+ end
41
+
42
+ it 'should accept currency/str' do
43
+ lambda{@bank.send(:same_currency?, Money::Currency.wrap('USD'), 'EUR')}.should_not raise_exception
44
+ end
45
+
46
+ it 'should accept str/currency' do
47
+ lambda{@bank.send(:same_currency?, 'USD', Money::Currency.wrap('EUR'))}.should_not raise_exception
48
+ end
49
+
50
+ it 'should accept currency/currency' do
51
+ lambda{@bank.send(:same_currency?, Money::Currency.wrap('USD'), Money::Currency.wrap('EUR'))}.should_not raise_exception
52
+ end
53
+
54
+ it 'should return `true` when currencies match' do
55
+ @bank.send(:same_currency?, 'USD', 'USD').should == true
56
+ @bank.send(:same_currency?, Money::Currency.wrap('USD'), 'USD').should == true
57
+ @bank.send(:same_currency?, 'USD', Money::Currency.wrap('USD')).should == true
58
+ @bank.send(:same_currency?, Money::Currency.wrap('USD'), Money::Currency.wrap('USD')).should == true
59
+ end
60
+
61
+ it 'should return `false` when currencies do not match' do
62
+ @bank.send(:same_currency?, 'USD', 'EUR').should == false
63
+ @bank.send(:same_currency?, Money::Currency.wrap('USD'), 'EUR').should == false
64
+ @bank.send(:same_currency?, 'USD', Money::Currency.wrap('EUR')).should == false
65
+ @bank.send(:same_currency?, Money::Currency.wrap('USD'), Money::Currency.wrap('EUR')).should == false
66
+ end
67
+
68
+ it 'should raise an UnknownCurrency exception when an unknown currency is passed' do
69
+ lambda{@bank.send(:same_currency?, 'AAA', 'BBB')}.should raise_exception(Money::Currency::UnknownCurrency)
70
+ end
71
+ end
72
+ end