money 6.13.4 → 6.19.0
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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +73 -2
- data/LICENSE +17 -17
- data/README.md +128 -47
- data/config/currency_backwards_compatible.json +36 -0
- data/config/currency_iso.json +144 -33
- data/config/currency_non_iso.json +17 -0
- data/lib/money/bank/variable_exchange.rb +17 -7
- data/lib/money/currency.rb +9 -4
- data/lib/money/money/allocation.rb +36 -8
- data/lib/money/money/arithmetic.rb +18 -13
- data/lib/money/money/formatter.rb +22 -12
- data/lib/money/money/formatting_rules.rb +13 -1
- data/lib/money/money.rb +129 -54
- data/lib/money/rates_store/memory.rb +24 -23
- data/lib/money/version.rb +1 -1
- data/money.gemspec +1 -3
- metadata +8 -9
@@ -9,6 +9,7 @@
|
|
9
9
|
"subunit": "Satoshi",
|
10
10
|
"subunit_to_unit": 100000000,
|
11
11
|
"symbol_first": false,
|
12
|
+
"format": "%n %u",
|
12
13
|
"html_entity": "₿",
|
13
14
|
"decimal_mark": ".",
|
14
15
|
"thousands_separator": ",",
|
@@ -125,5 +126,21 @@
|
|
125
126
|
"thousands_separator": ",",
|
126
127
|
"iso_numeric": "",
|
127
128
|
"smallest_denomination": 1
|
129
|
+
},
|
130
|
+
"usdc": {
|
131
|
+
"priority": 100,
|
132
|
+
"iso_code": "USDC",
|
133
|
+
"name": "USD Coin",
|
134
|
+
"symbol": "USDC",
|
135
|
+
"disambiguate_symbol": "USDC",
|
136
|
+
"alternate_symbols": [],
|
137
|
+
"subunit": "Cent",
|
138
|
+
"subunit_to_unit": 100,
|
139
|
+
"symbol_first": false,
|
140
|
+
"html_entity": "$",
|
141
|
+
"decimal_mark": ".",
|
142
|
+
"thousands_separator": ",",
|
143
|
+
"iso_numeric": "",
|
144
|
+
"smallest_denomination": 1
|
128
145
|
}
|
129
146
|
}
|
@@ -42,7 +42,7 @@ class Money
|
|
42
42
|
# bank.get_rate 'USD', 'CAD'
|
43
43
|
class VariableExchange < Base
|
44
44
|
|
45
|
-
attr_reader :mutex
|
45
|
+
attr_reader :mutex
|
46
46
|
|
47
47
|
# Available formats for importing/exporting rates.
|
48
48
|
RATE_FORMATS = [:json, :ruby, :yaml].freeze
|
@@ -61,6 +61,10 @@ class Money
|
|
61
61
|
super(&block)
|
62
62
|
end
|
63
63
|
|
64
|
+
def store
|
65
|
+
@store.is_a?(String) ? Object.const_get(@store) : @store
|
66
|
+
end
|
67
|
+
|
64
68
|
def marshal_dump
|
65
69
|
[store.marshal_dump, @rounding_method]
|
66
70
|
end
|
@@ -109,8 +113,10 @@ class Money
|
|
109
113
|
else
|
110
114
|
if rate = get_rate(from.currency, to_currency)
|
111
115
|
fractional = calculate_fractional(from, to_currency)
|
112
|
-
from.
|
113
|
-
exchange(fractional, rate, &block),
|
116
|
+
from.dup_with(
|
117
|
+
fractional: exchange(fractional, rate, &block),
|
118
|
+
currency: to_currency,
|
119
|
+
bank: self
|
114
120
|
)
|
115
121
|
else
|
116
122
|
raise UnknownRate, "No conversion rate known for '#{from.currency.iso_code}' -> '#{to_currency}'"
|
@@ -213,8 +219,7 @@ class Money
|
|
213
219
|
# s = bank.export_rates(:json)
|
214
220
|
# s #=> "{\"USD_TO_CAD\":1.24515,\"CAD_TO_USD\":0.803115}"
|
215
221
|
def export_rates(format, file = nil, opts = {})
|
216
|
-
raise Money::Bank::UnknownRateFormat unless
|
217
|
-
RATE_FORMATS.include? format
|
222
|
+
raise Money::Bank::UnknownRateFormat unless RATE_FORMATS.include?(format)
|
218
223
|
|
219
224
|
store.transaction do
|
220
225
|
s = FORMAT_SERIALIZERS[format].dump(rates)
|
@@ -254,8 +259,13 @@ class Money
|
|
254
259
|
# bank.get_rate("USD", "CAD") #=> 1.24515
|
255
260
|
# bank.get_rate("CAD", "USD") #=> 0.803115
|
256
261
|
def import_rates(format, s, opts = {})
|
257
|
-
raise Money::Bank::UnknownRateFormat unless
|
258
|
-
|
262
|
+
raise Money::Bank::UnknownRateFormat unless RATE_FORMATS.include?(format)
|
263
|
+
|
264
|
+
if format == :ruby
|
265
|
+
warn '[WARNING] Using :ruby format when importing rates is potentially unsafe and ' \
|
266
|
+
'might lead to remote code execution via Marshal.load deserializer. Consider using ' \
|
267
|
+
'safe alternatives such as :json and :yaml.'
|
268
|
+
end
|
259
269
|
|
260
270
|
store.transaction do
|
261
271
|
data = FORMAT_SERIALIZERS[format].load(s)
|
data/lib/money/currency.rb
CHANGED
@@ -128,7 +128,7 @@ class Money
|
|
128
128
|
# @return [Array]
|
129
129
|
#
|
130
130
|
# @example
|
131
|
-
# Money::Currency.
|
131
|
+
# Money::Currency.all()
|
132
132
|
# [#<Currency ..USD>, 'CAD', 'EUR']...
|
133
133
|
def all
|
134
134
|
table.keys.map do |curr|
|
@@ -206,6 +206,10 @@ class Money
|
|
206
206
|
all.each { |c| yield(c) }
|
207
207
|
end
|
208
208
|
|
209
|
+
def reset!
|
210
|
+
@@instances = {}
|
211
|
+
@table = Loader.load_currencies
|
212
|
+
end
|
209
213
|
|
210
214
|
private
|
211
215
|
|
@@ -253,7 +257,7 @@ class Money
|
|
253
257
|
|
254
258
|
attr_reader :id, :priority, :iso_code, :iso_numeric, :name, :symbol,
|
255
259
|
:disambiguate_symbol, :html_entity, :subunit, :subunit_to_unit, :decimal_mark,
|
256
|
-
:thousands_separator, :symbol_first, :smallest_denomination
|
260
|
+
:thousands_separator, :symbol_first, :smallest_denomination, :format
|
257
261
|
|
258
262
|
alias_method :separator, :decimal_mark
|
259
263
|
alias_method :delimiter, :thousands_separator
|
@@ -341,7 +345,7 @@ class Money
|
|
341
345
|
# @example
|
342
346
|
# Money::Currency.new(:usd) #=> #<Currency id: usd ...>
|
343
347
|
def inspect
|
344
|
-
"#<#{self.class.name} id: #{id}, priority: #{priority}, symbol_first: #{symbol_first}, thousands_separator: #{thousands_separator}, html_entity: #{html_entity}, decimal_mark: #{decimal_mark}, name: #{name}, symbol: #{symbol}, subunit_to_unit: #{subunit_to_unit}, exponent: #{exponent}, iso_code: #{iso_code}, iso_numeric: #{iso_numeric}, subunit: #{subunit}, smallest_denomination: #{smallest_denomination}>"
|
348
|
+
"#<#{self.class.name} id: #{id}, priority: #{priority}, symbol_first: #{symbol_first}, thousands_separator: #{thousands_separator}, html_entity: #{html_entity}, decimal_mark: #{decimal_mark}, name: #{name}, symbol: #{symbol}, subunit_to_unit: #{subunit_to_unit}, exponent: #{exponent}, iso_code: #{iso_code}, iso_numeric: #{iso_numeric}, subunit: #{subunit}, smallest_denomination: #{smallest_denomination}, format: #{format}>"
|
345
349
|
end
|
346
350
|
|
347
351
|
# Returns a string representation corresponding to the upcase +id+
|
@@ -414,7 +418,7 @@ class Money
|
|
414
418
|
|
415
419
|
# Returns the relation between subunit and unit as a base 10 exponent.
|
416
420
|
#
|
417
|
-
# Note that MGA and
|
421
|
+
# Note that MGA and MRU are exceptions and are rounded to 1
|
418
422
|
# @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes
|
419
423
|
#
|
420
424
|
# @return [Integer]
|
@@ -441,6 +445,7 @@ class Money
|
|
441
445
|
@symbol = data[:symbol]
|
442
446
|
@symbol_first = data[:symbol_first]
|
443
447
|
@thousands_separator = data[:thousands_separator]
|
448
|
+
@format = data[:format]
|
444
449
|
end
|
445
450
|
end
|
446
451
|
end
|
@@ -2,20 +2,32 @@
|
|
2
2
|
|
3
3
|
class Money
|
4
4
|
class Allocation
|
5
|
-
# Splits a given amount in parts
|
6
|
-
#
|
7
|
-
# parties listed first will likely receive more pennies than ones that are listed later.
|
5
|
+
# Splits a given amount in parts. The allocation is based on the parts' proportions
|
6
|
+
# or evenly if parts are numerically specified.
|
8
7
|
#
|
9
8
|
# The results should always add up to the original amount.
|
10
9
|
#
|
11
|
-
# The
|
12
|
-
#
|
13
|
-
#
|
10
|
+
# @param amount [Numeric] The total amount to be allocated.
|
11
|
+
# @param parts [Numeric, Array<Numeric>] Number of parts to split into or an array (proportions for allocation)
|
12
|
+
# @param whole_amounts [Boolean] Specifies whether to allocate whole amounts only. Defaults to true.
|
14
13
|
#
|
14
|
+
# @return [Array<Numeric>] An array containing the allocated amounts.
|
15
|
+
# @raise [ArgumentError] If parts is empty or not provided.
|
15
16
|
def self.generate(amount, parts, whole_amounts = true)
|
16
|
-
parts = parts.is_a?(Numeric)
|
17
|
+
parts = if parts.is_a?(Numeric)
|
18
|
+
Array.new(parts, 1)
|
19
|
+
elsif parts.all?(&:zero?)
|
20
|
+
Array.new(parts.count, 1)
|
21
|
+
else
|
22
|
+
parts.dup
|
23
|
+
end
|
24
|
+
|
25
|
+
raise ArgumentError, 'need at least one part' if parts.empty?
|
17
26
|
|
18
|
-
|
27
|
+
if [amount, *parts].any? { |i| i.is_a?(BigDecimal) || i.is_a?(Float) || i.is_a?(Rational) }
|
28
|
+
amount = convert_to_big_decimal(amount)
|
29
|
+
parts.map! { |p| convert_to_big_decimal(p) }
|
30
|
+
end
|
19
31
|
|
20
32
|
result = []
|
21
33
|
remaining_amount = amount
|
@@ -36,5 +48,21 @@ class Money
|
|
36
48
|
|
37
49
|
result
|
38
50
|
end
|
51
|
+
|
52
|
+
# Converts a given number to BigDecimal.
|
53
|
+
# This method supports inputs of BigDecimal, Rational, and other numeric types by ensuring they are all returned
|
54
|
+
# as BigDecimal instances for consistent handling.
|
55
|
+
#
|
56
|
+
# @param number [Numeric, BigDecimal, Rational] The number to convert.
|
57
|
+
# @return [BigDecimal] The converted number as a BigDecimal.
|
58
|
+
def self.convert_to_big_decimal(number)
|
59
|
+
if number.is_a? BigDecimal
|
60
|
+
number
|
61
|
+
elsif number.is_a? Rational
|
62
|
+
BigDecimal(number.to_f.to_s)
|
63
|
+
else
|
64
|
+
BigDecimal(number.to_s)
|
65
|
+
end
|
66
|
+
end
|
39
67
|
end
|
40
68
|
end
|
@@ -16,7 +16,7 @@ class Money
|
|
16
16
|
# @example
|
17
17
|
# - Money.new(100) #=> #<Money @fractional=-100>
|
18
18
|
def -@
|
19
|
-
|
19
|
+
dup_with(fractional: -fractional)
|
20
20
|
end
|
21
21
|
|
22
22
|
# Checks whether two Money objects have the same currency and the same
|
@@ -46,7 +46,7 @@ class Money
|
|
46
46
|
# Compares two Money objects. If money objects have a different currency it
|
47
47
|
# will attempt to convert the currency.
|
48
48
|
#
|
49
|
-
# @param [Money]
|
49
|
+
# @param [Money] other Value to compare with.
|
50
50
|
#
|
51
51
|
# @return [Integer]
|
52
52
|
#
|
@@ -57,7 +57,12 @@ class Money
|
|
57
57
|
return unless other.respond_to?(:zero?) && other.zero?
|
58
58
|
return other.is_a?(CoercedNumeric) ? 0 <=> fractional : fractional <=> 0
|
59
59
|
end
|
60
|
-
|
60
|
+
|
61
|
+
# Always allow comparison with zero
|
62
|
+
if zero? || other.zero?
|
63
|
+
return fractional <=> other.fractional
|
64
|
+
end
|
65
|
+
|
61
66
|
other = other.exchange_to(currency)
|
62
67
|
fractional <=> other.fractional
|
63
68
|
rescue Money::Bank::UnknownRate
|
@@ -103,7 +108,7 @@ class Money
|
|
103
108
|
# values. If +other_money+ has a different currency then its monetary value
|
104
109
|
# is automatically exchanged to this object's currency using +exchange_to+.
|
105
110
|
#
|
106
|
-
# @param [Money]
|
111
|
+
# @param [Money] other Other +Money+ object to add.
|
107
112
|
#
|
108
113
|
# @return [Money]
|
109
114
|
#
|
@@ -116,7 +121,7 @@ class Money
|
|
116
121
|
# its monetary value is automatically exchanged to this object's currency
|
117
122
|
# using +exchange_to+.
|
118
123
|
#
|
119
|
-
# @param [Money]
|
124
|
+
# @param [Money] other Other +Money+ object to subtract.
|
120
125
|
#
|
121
126
|
# @return [Money]
|
122
127
|
#
|
@@ -132,10 +137,10 @@ class Money
|
|
132
137
|
when Money
|
133
138
|
other = other.exchange_to(currency)
|
134
139
|
new_fractional = fractional.public_send(op, other.fractional)
|
135
|
-
|
140
|
+
dup_with(fractional: new_fractional)
|
136
141
|
when CoercedNumeric
|
137
142
|
raise TypeError, non_zero_message.call(other.value) unless other.zero?
|
138
|
-
|
143
|
+
dup_with(fractional: other.value.public_send(op, fractional))
|
139
144
|
when Numeric
|
140
145
|
raise TypeError, non_zero_message.call(other) unless other.zero?
|
141
146
|
self
|
@@ -162,7 +167,7 @@ class Money
|
|
162
167
|
def *(value)
|
163
168
|
value = value.value if value.is_a?(CoercedNumeric)
|
164
169
|
if value.is_a? Numeric
|
165
|
-
|
170
|
+
dup_with(fractional: fractional * value)
|
166
171
|
else
|
167
172
|
raise TypeError, "Can't multiply a #{self.class.name} by a #{value.class.name}'s value"
|
168
173
|
end
|
@@ -188,7 +193,7 @@ class Money
|
|
188
193
|
fractional / as_d(value.exchange_to(currency).fractional).to_f
|
189
194
|
else
|
190
195
|
raise TypeError, 'Can not divide by Money' if value.is_a?(CoercedNumeric)
|
191
|
-
|
196
|
+
dup_with(fractional: fractional / as_d(value))
|
192
197
|
end
|
193
198
|
end
|
194
199
|
|
@@ -226,13 +231,13 @@ class Money
|
|
226
231
|
def divmod_money(val)
|
227
232
|
cents = val.exchange_to(currency).cents
|
228
233
|
quotient, remainder = fractional.divmod(cents)
|
229
|
-
[quotient,
|
234
|
+
[quotient, dup_with(fractional: remainder)]
|
230
235
|
end
|
231
236
|
private :divmod_money
|
232
237
|
|
233
238
|
def divmod_other(val)
|
234
239
|
quotient, remainder = fractional.divmod(as_d(val))
|
235
|
-
[
|
240
|
+
[dup_with(fractional: quotient), dup_with(fractional: remainder)]
|
236
241
|
end
|
237
242
|
private :divmod_other
|
238
243
|
|
@@ -276,7 +281,7 @@ class Money
|
|
276
281
|
if (fractional < 0 && val < 0) || (fractional > 0 && val > 0)
|
277
282
|
self.modulo(val)
|
278
283
|
else
|
279
|
-
self.modulo(val) - (val.is_a?(Money) ? val :
|
284
|
+
self.modulo(val) - (val.is_a?(Money) ? val : dup_with(fractional: val))
|
280
285
|
end
|
281
286
|
end
|
282
287
|
|
@@ -287,7 +292,7 @@ class Money
|
|
287
292
|
# @example
|
288
293
|
# Money.new(-100).abs #=> #<Money @fractional=100>
|
289
294
|
def abs
|
290
|
-
|
295
|
+
dup_with(fractional: fractional.abs)
|
291
296
|
end
|
292
297
|
|
293
298
|
# Test if the money amount is zero.
|
@@ -124,11 +124,14 @@ class Money
|
|
124
124
|
# the currency should be delimited by the specified character or ','
|
125
125
|
#
|
126
126
|
# @example
|
127
|
-
# # If
|
127
|
+
# # If a falsey value is specified, no thousands_separator is used.
|
128
128
|
# Money.new(100000, "USD").format(thousands_separator: false) #=> "1000.00"
|
129
129
|
# Money.new(100000, "USD").format(thousands_separator: nil) #=> "1000.00"
|
130
130
|
# Money.new(100000, "USD").format(thousands_separator: "") #=> "1000.00"
|
131
131
|
#
|
132
|
+
# # If true is specified, the locale or default thousands_separator is used.
|
133
|
+
# Money.new(100000, "USD").format(thousands_separator: true) #=> "1,000.00"
|
134
|
+
#
|
132
135
|
# # If a string is specified, it's value is used.
|
133
136
|
# Money.new(100000, "USD").format(thousands_separator: ".") #=> "$1.000.00"
|
134
137
|
#
|
@@ -211,6 +214,12 @@ class Money
|
|
211
214
|
# Money.new(89000, :btc).format(drop_trailing_zeros: true) #=> B⃦0.00089
|
212
215
|
# Money.new(110, :usd).format(drop_trailing_zeros: true) #=> $1.1
|
213
216
|
#
|
217
|
+
# @option rules [Boolean] :delimiter_pattern (/(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/) Regular expression to set the placement
|
218
|
+
# for the thousands delimiter
|
219
|
+
#
|
220
|
+
# @example
|
221
|
+
# Money.new(89000, :btc).format(delimiter_pattern: /(\d)(?=\d)/) #=> B⃦8,9,0.00
|
222
|
+
#
|
214
223
|
# @option rules [String] :format (nil) Provide a template for formatting. `%u` will be replaced
|
215
224
|
# with the symbol (if present) and `%n` will be replaced with the number.
|
216
225
|
#
|
@@ -235,7 +244,11 @@ class Money
|
|
235
244
|
end
|
236
245
|
|
237
246
|
def thousands_separator
|
238
|
-
lookup :thousands_separator
|
247
|
+
val = lookup :thousands_separator
|
248
|
+
|
249
|
+
return val unless val == true
|
250
|
+
|
251
|
+
lookup_default :thousands_separator
|
239
252
|
end
|
240
253
|
|
241
254
|
def decimal_mark
|
@@ -330,7 +343,9 @@ class Money
|
|
330
343
|
|
331
344
|
def format_whole_part(value)
|
332
345
|
# Apply thousands_separator
|
333
|
-
value.gsub
|
346
|
+
value.gsub(rules[:delimiter_pattern]) do |digit_to_delimit|
|
347
|
+
"#{digit_to_delimit}#{thousands_separator}"
|
348
|
+
end
|
334
349
|
end
|
335
350
|
|
336
351
|
def extract_whole_and_decimal_parts
|
@@ -347,7 +362,7 @@ class Money
|
|
347
362
|
end
|
348
363
|
|
349
364
|
def format_decimal_part(value)
|
350
|
-
return nil if currency.decimal_places == 0 && !Money.
|
365
|
+
return nil if currency.decimal_places == 0 && !Money.default_infinite_precision
|
351
366
|
return nil if rules[:no_cents]
|
352
367
|
return nil if rules[:no_cents_if_whole] && value.to_i == 0
|
353
368
|
|
@@ -363,16 +378,11 @@ class Money
|
|
363
378
|
def lookup(key)
|
364
379
|
return rules[key] || DEFAULTS[key] if rules.has_key?(key)
|
365
380
|
|
366
|
-
|
381
|
+
lookup_default key
|
367
382
|
end
|
368
383
|
|
369
|
-
def
|
370
|
-
|
371
|
-
# from http://blog.revathskumar.com/2014/11/regex-comma-seperated-indian-currency-format.html
|
372
|
-
/(\d+?)(?=(\d\d)+(\d)(?!\d))(\.\d+)?/
|
373
|
-
else
|
374
|
-
/(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/
|
375
|
-
end
|
384
|
+
def lookup_default(key)
|
385
|
+
(Money.locale_backend && Money.locale_backend.lookup(key, currency)) || DEFAULTS[key]
|
376
386
|
end
|
377
387
|
|
378
388
|
def symbol_value_from(rules)
|
@@ -12,6 +12,7 @@ class Money
|
|
12
12
|
@rules = localize_formatting_rules(@rules)
|
13
13
|
@rules = translate_formatting_rules(@rules) if @rules[:translate]
|
14
14
|
@rules[:format] ||= determine_format_from_formatting_rules(@rules)
|
15
|
+
@rules[:delimiter_pattern] ||= delimiter_pattern_rule(@rules)
|
15
16
|
|
16
17
|
warn_about_deprecated_rules(@rules)
|
17
18
|
end
|
@@ -79,6 +80,8 @@ class Money
|
|
79
80
|
end
|
80
81
|
|
81
82
|
def determine_format_from_formatting_rules(rules)
|
83
|
+
return currency.format if currency.format && !rules.has_key?(:symbol_position)
|
84
|
+
|
82
85
|
symbol_position = symbol_position_from(rules)
|
83
86
|
|
84
87
|
if symbol_position == :before
|
@@ -88,6 +91,15 @@ class Money
|
|
88
91
|
end
|
89
92
|
end
|
90
93
|
|
94
|
+
def delimiter_pattern_rule(rules)
|
95
|
+
if rules[:south_asian_number_formatting]
|
96
|
+
# from http://blog.revathskumar.com/2014/11/regex-comma-seperated-indian-currency-format.html
|
97
|
+
/(\d+?)(?=(\d\d)+(\d)(?!\d))(\.\d+)?/
|
98
|
+
else
|
99
|
+
/(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
91
103
|
def symbol_position_from(rules)
|
92
104
|
if rules.has_key?(:symbol_position)
|
93
105
|
if [:before, :after].include?(rules[:symbol_position])
|
@@ -105,7 +117,7 @@ class Money
|
|
105
117
|
def warn_about_deprecated_rules(rules)
|
106
118
|
if rules.has_key?(:symbol_position)
|
107
119
|
position = rules[:symbol_position]
|
108
|
-
template = position == :before ? '%u
|
120
|
+
template = position == :before ? '%u%n' : '%n%u'
|
109
121
|
|
110
122
|
warn "[DEPRECATION] `symbol_position: :#{position}` is deprecated - you can replace it with `format: #{template}`"
|
111
123
|
end
|