money 3.7.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/CHANGELOG.md +384 -351
  2. data/LICENSE +21 -21
  3. data/README.md +243 -214
  4. data/Rakefile +49 -49
  5. data/lib/money.rb +28 -27
  6. data/lib/money/bank/base.rb +131 -131
  7. data/lib/money/bank/variable_exchange.rb +252 -252
  8. data/lib/money/core_extensions.rb +82 -82
  9. data/lib/money/currency.rb +263 -422
  10. data/lib/money/currency_loader.rb +19 -0
  11. data/lib/money/money.rb +405 -405
  12. data/lib/money/money/arithmetic.rb +246 -246
  13. data/lib/money/money/formatting.rb +260 -244
  14. data/lib/money/money/parsing.rb +350 -350
  15. data/money.gemspec +29 -35
  16. data/spec/bank/base_spec.rb +72 -72
  17. data/spec/bank/variable_exchange_spec.rb +238 -238
  18. data/spec/core_extensions_spec.rb +158 -158
  19. data/spec/currency_spec.rb +120 -133
  20. data/spec/money/arithmetic_spec.rb +479 -479
  21. data/spec/money/formatting_spec.rb +383 -375
  22. data/spec/money/parsing_spec.rb +197 -197
  23. data/spec/money_spec.rb +292 -292
  24. data/spec/spec_helper.rb +28 -28
  25. metadata +54 -126
  26. data/lib/money.rbc +0 -184
  27. data/lib/money/bank/base.rbc +0 -818
  28. data/lib/money/bank/variable_exchange.rbc +0 -2550
  29. data/lib/money/core_extensions.rbc +0 -664
  30. data/lib/money/currency.rbc +0 -22708
  31. data/lib/money/money.rbc +0 -3861
  32. data/lib/money/money/arithmetic.rbc +0 -2778
  33. data/lib/money/money/formatting.rbc +0 -2265
  34. data/lib/money/money/parsing.rbc +0 -2737
  35. data/spec/bank/base_spec.rbc +0 -2461
  36. data/spec/bank/variable_exchange_spec.rbc +0 -7541
  37. data/spec/core_extensions_spec.rbc +0 -5921
  38. data/spec/currency_spec.rbc +0 -4535
  39. data/spec/money/arithmetic_spec.rbc +0 -25140
  40. data/spec/money/formatting_spec.rbc +0 -12545
  41. data/spec/money/parsing_spec.rbc +0 -6511
  42. data/spec/money_spec.rbc +0 -9824
  43. data/spec/spec_helper.rbc +0 -575
@@ -1,350 +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
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