money 3.6.1 → 3.6.2

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.
@@ -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