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.
@@ -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, :store
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.class.new(
113
- exchange(fractional, rate, &block), to_currency
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
- RATE_FORMATS.include? format
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)
@@ -128,7 +128,7 @@ class Money
128
128
  # @return [Array]
129
129
  #
130
130
  # @example
131
- # Money::Currency.iso_codes()
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 MRO are exceptions and are rounded to 1
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 without loosing pennies.
6
- # The left-over pennies will be distributed round-robin amongst the parties. This means that
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 parts can be specified as:
12
- # Numeric performs the split between a given number of parties evenely
13
- # Array<Numeric> allocates the amounts proportionally to the given array
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) ? Array.new(parts, 1) : parts.dup
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
- raise ArgumentError, 'need at least one party' if parts.empty?
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
- self.class.new(-fractional, currency, bank)
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] other_money Value to compare with.
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
- return 0 if zero? && other.zero?
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] other_money Other +Money+ object to add.
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] other_money Other +Money+ object to subtract.
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
- self.class.new(new_fractional, currency, bank)
140
+ dup_with(fractional: new_fractional)
136
141
  when CoercedNumeric
137
142
  raise TypeError, non_zero_message.call(other.value) unless other.zero?
138
- self.class.new(other.value.public_send(op, fractional), currency)
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
- self.class.new(fractional * value, currency, bank)
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
- self.class.new(fractional / as_d(value), currency, bank)
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, self.class.new(remainder, currency, bank)]
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
- [self.class.new(quotient, currency, bank), self.class.new(remainder, currency, bank)]
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 : self.class.new(val, currency, bank))
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
- self.class.new(fractional.abs, currency, bank)
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 false is specified, no thousands_separator is used.
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 regexp_format, "\\1#{thousands_separator}"
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.infinite_precision
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
- (Money.locale_backend && Money.locale_backend.lookup(key, currency)) || DEFAULTS[key]
381
+ lookup_default key
367
382
  end
368
383
 
369
- def regexp_format
370
- if rules[:south_asian_number_formatting]
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 %n' : '%n %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