money 6.13.5 → 6.14.1

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": ",",
@@ -113,8 +113,10 @@ class Money
113
113
  else
114
114
  if rate = get_rate(from.currency, to_currency)
115
115
  fractional = calculate_fractional(from, to_currency)
116
- from.class.new(
117
- exchange(fractional, rate, &block), to_currency
116
+ from.dup_with(
117
+ fractional: exchange(fractional, rate, &block),
118
+ currency: to_currency,
119
+ bank: self
118
120
  )
119
121
  else
120
122
  raise UnknownRate, "No conversion rate known for '#{from.currency.iso_code}' -> '#{to_currency}'"
@@ -217,8 +219,7 @@ class Money
217
219
  # s = bank.export_rates(:json)
218
220
  # s #=> "{\"USD_TO_CAD\":1.24515,\"CAD_TO_USD\":0.803115}"
219
221
  def export_rates(format, file = nil, opts = {})
220
- raise Money::Bank::UnknownRateFormat unless
221
- RATE_FORMATS.include? format
222
+ raise Money::Bank::UnknownRateFormat unless RATE_FORMATS.include?(format)
222
223
 
223
224
  store.transaction do
224
225
  s = FORMAT_SERIALIZERS[format].dump(rates)
@@ -258,8 +259,13 @@ class Money
258
259
  # bank.get_rate("USD", "CAD") #=> 1.24515
259
260
  # bank.get_rate("CAD", "USD") #=> 0.803115
260
261
  def import_rates(format, s, opts = {})
261
- raise Money::Bank::UnknownRateFormat unless
262
- 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
263
269
 
264
270
  store.transaction do
265
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+
@@ -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
data/lib/money/money.rb CHANGED
@@ -91,38 +91,57 @@ class Money
91
91
  class << self
92
92
 
93
93
  # @!attribute [rw] default_bank
94
- # @return [Money::Bank::Base] Each Money object is associated to a bank
95
- # object, which is responsible for currency exchange. This property
96
- # allows you to specify the default bank object. The default value for
97
- # this property is an instance of +Bank::VariableExchange.+ It allows
98
- # one to specify custom exchange rates.
94
+ # Used to set a default bank for currency exchange.
95
+ #
96
+ # Each Money object is associated with a bank
97
+ # object, which is responsible for currency exchange. This property
98
+ # allows you to specify the default bank object. The default value for
99
+ # this property is an instance of +Bank::VariableExchange.+ It allows
100
+ # one to specify custom exchange rates.
101
+ #
102
+ # @return [Money::Bank::Base]
99
103
  #
100
104
  # @!attribute default_formatting_rules
101
- # @return [Hash] Use this to define a default hash of rules for every time
102
- # +Money#format+ is called. Rules provided on method call will be
103
- # merged with the default ones. To overwrite a rule, just provide the
104
- # intended value while calling +format+.
105
+ # Used to define a default hash of rules for every time
106
+ # +Money#format+ is called. Rules provided on method call will be
107
+ # merged with the default ones. To overwrite a rule, just provide the
108
+ # intended value while calling +format+.
105
109
  #
106
- # @see +Money::Formatting#format+ for more details.
110
+ # @see Money::Formatter#initialize Money::Formatter for more details
107
111
  #
108
112
  # @example
109
113
  # Money.default_formatting_rules = { display_free: true }
110
114
  # Money.new(0, "USD").format # => "free"
111
115
  # Money.new(0, "USD").format(display_free: false) # => "$0.00"
112
116
  #
117
+ # @return [Hash]
118
+ #
113
119
  # @!attribute [rw] use_i18n
114
- # @return [Boolean] Use this to disable i18n even if it's used by other
115
- # objects in your app.
120
+ # Used to disable i18n even if it's used by other components of your app.
116
121
  #
117
- # @!attribute [rw] infinite_precision
118
- # @return [Boolean] Use this to enable infinite precision cents
122
+ # @return [Boolean]
123
+ #
124
+ # @!attribute [rw] default_infinite_precision
125
+ # @return [Boolean] Use this to enable infinite precision cents as the
126
+ # global default
119
127
  #
120
128
  # @!attribute [rw] conversion_precision
121
- # @return [Integer] Use this to specify precision for converting Rational
122
- # to BigDecimal
123
- attr_accessor :default_bank, :default_formatting_rules,
124
- :use_i18n, :infinite_precision, :conversion_precision,
125
- :locale_backend
129
+ # Used to specify precision for converting Rational to BigDecimal
130
+ #
131
+ # @return [Integer]
132
+ attr_accessor :default_formatting_rules, :default_infinite_precision, :conversion_precision
133
+ attr_reader :use_i18n, :locale_backend
134
+ attr_writer :default_bank
135
+
136
+ def infinite_precision
137
+ warn '[DEPRECATION] `Money.infinite_precision` is deprecated - use `Money.default_infinite_precision` instead'
138
+ default_infinite_precision
139
+ end
140
+
141
+ def infinite_precision=(value)
142
+ warn '[DEPRECATION] `Money.infinite_precision=` is deprecated - use `Money.default_infinite_precision= ` instead'
143
+ self.default_infinite_precision = value
144
+ end
126
145
  end
127
146
 
128
147
  # @!attribute default_currency
@@ -132,8 +151,9 @@ class Money
132
151
  # +Money::Currency+ instance.
133
152
  def self.default_currency
134
153
  if @using_deprecated_default_currency
135
- warn '[WARNING] The default currency will change to `nil` in the next major release. Make ' \
154
+ warn '[WARNING] The default currency will change from `USD` to `nil` in the next major release. Make ' \
136
155
  'sure to set it explicitly using `Money.default_currency=` to avoid potential issues'
156
+ @using_deprecated_default_currency = false
137
157
  end
138
158
 
139
159
  if @default_currency.respond_to?(:call)
@@ -148,6 +168,14 @@ class Money
148
168
  @default_currency = currency
149
169
  end
150
170
 
171
+ def self.default_bank
172
+ if @default_bank.respond_to?(:call)
173
+ @default_bank.call
174
+ else
175
+ @default_bank
176
+ end
177
+ end
178
+
151
179
  def self.locale_backend=(value)
152
180
  @locale_backend = value ? LocaleBackend.find(value) : nil
153
181
  end
@@ -183,7 +211,7 @@ class Money
183
211
  self.locale_backend = :legacy
184
212
 
185
213
  # Default to not using infinite precision cents
186
- self.infinite_precision = false
214
+ self.default_infinite_precision = false
187
215
 
188
216
  # Default to bankers rounding
189
217
  self.rounding_mode = BigDecimal::ROUND_HALF_EVEN
@@ -213,19 +241,22 @@ class Money
213
241
  return Thread.current[:money_rounding_mode] if Thread.current[:money_rounding_mode]
214
242
 
215
243
  if @using_deprecated_default_rounding_mode
216
- warn '[WARNING] The default rounding mode will change to `ROUND_HALF_UP` in the next major ' \
217
- 'release. Set it explicitly using `Money.rounding_mode=` to avoid potential problems.'
244
+ warn '[WARNING] The default rounding mode will change from `ROUND_HALF_EVEN` to `ROUND_HALF_UP` in the ' \
245
+ 'next major release. Set it explicitly using `Money.rounding_mode=` to avoid potential problems.'
246
+ @using_deprecated_default_rounding_mode = false
218
247
  end
219
248
 
220
249
  @rounding_mode
221
250
  end
222
251
 
223
- # This method temporarily changes the rounding mode. It will then return the
224
- # results of the block instead.
252
+ # Temporarily changes the rounding mode in a given block.
225
253
  #
226
254
  # @param [BigDecimal::ROUND_MODE] mode
227
255
  #
228
- # @return [BigDecimal::ROUND_MODE,Yield] block results
256
+ # @yield The block within which rounding mode will be changed. Its return
257
+ # value will also be the return value of the whole method.
258
+ #
259
+ # @return [Object] block results
229
260
  #
230
261
  # @example
231
262
  # fee = Money.with_rounding_mode(BigDecimal::ROUND_HALF_UP) do
@@ -263,7 +294,8 @@ class Money
263
294
  #
264
295
  # @param [Numeric] amount The numerical value of the money.
265
296
  # @param [Currency, String, Symbol] currency The currency format.
266
- # @param [Money::Bank::*] bank The exchange bank to use.
297
+ # @param [Hash] options Optional settings for the new Money instance
298
+ # @option [Money::Bank::*] :bank The exchange bank to use.
267
299
  #
268
300
  # @example
269
301
  # Money.from_amount(23.45, "USD") # => #<Money fractional:2345 currency:USD>
@@ -272,13 +304,12 @@ class Money
272
304
  # @return [Money]
273
305
  #
274
306
  # @see #initialize
275
- def self.from_amount(amount, currency = default_currency, bank = default_bank)
307
+ def self.from_amount(amount, currency = default_currency, options = {})
276
308
  raise ArgumentError, "'amount' must be numeric" unless Numeric === amount
277
309
 
278
310
  currency = Currency.wrap(currency) || Money.default_currency
279
311
  value = amount.to_d * currency.subunit_to_unit
280
- value = value.round(0, rounding_mode) unless infinite_precision
281
- new(value, currency, bank)
312
+ new(value, currency, options)
282
313
  end
283
314
 
284
315
  # Creates a new Money object of value given in the
@@ -292,7 +323,8 @@ class Money
292
323
  # argument, a Money will be created in that currency with fractional value
293
324
  # = 0.
294
325
  # @param [Currency, String, Symbol] currency The currency format.
295
- # @param [Money::Bank::*] bank The exchange bank to use.
326
+ # @param [Hash] options Optional settings for the new Money instance
327
+ # @option [Money::Bank::*] :bank The exchange bank to use.
296
328
  #
297
329
  # @return [Money]
298
330
  #
@@ -301,11 +333,17 @@ class Money
301
333
  # Money.new(100, "USD") #=> #<Money @fractional=100 @currency="USD">
302
334
  # Money.new(100, "EUR") #=> #<Money @fractional=100 @currency="EUR">
303
335
  #
304
- def initialize(obj, currency = Money.default_currency, bank = Money.default_bank)
336
+ def initialize( obj, currency = Money.default_currency, options = {})
337
+ # For backwards compatability, if options is not a Hash, treat it as a bank parameter
338
+ unless options.is_a?(Hash)
339
+ options = { bank: options }
340
+ end
341
+
305
342
  @fractional = as_d(obj.respond_to?(:fractional) ? obj.fractional : obj)
306
343
  @currency = obj.respond_to?(:currency) ? obj.currency : Currency.wrap(currency)
307
344
  @currency ||= Money.default_currency
308
- @bank = obj.respond_to?(:bank) ? obj.bank : bank
345
+ @bank = obj.respond_to?(:bank) ? obj.bank : options[:bank]
346
+ @bank ||= Money.default_bank
309
347
 
310
348
  # BigDecimal can be Infinity and NaN, money of that amount does not make sense
311
349
  raise ArgumentError, 'must be initialized with a finite value' unless @fractional.finite?
@@ -454,7 +492,7 @@ class Money
454
492
  if !new_currency || currency == new_currency
455
493
  self
456
494
  else
457
- self.class.new(fractional, new_currency, bank)
495
+ dup_with(currency: new_currency)
458
496
  end
459
497
  end
460
498
 
@@ -531,13 +569,15 @@ class Money
531
569
  exchange_to("EUR")
532
570
  end
533
571
 
534
- # Splits a given amount in parts without loosing pennies. The left-over pennies will be
535
- # distributed round-robin amongst the parties. This means that parties listed first will likely
536
- # receive more pennies than ones that are listed later.
572
+ # Splits a given amount in parts without losing pennies. The left-over pennies will be
573
+ # distributed round-robin amongst the parties. This means that parts listed first will likely
574
+ # receive more pennies than ones listed later.
575
+ #
576
+ # Pass [2, 1, 1] as input to give twice as much to part1 as part2 or
577
+ # part3 which results in 50% of the cash to party1, 25% to part2, and 25% to part3. Passing a
578
+ # number instead of an array will split the amount evenly (without losing pennies when rounding).
537
579
  #
538
- # @param [Array<Numeric>, Numeric] pass [2, 1, 1] to give twice as much to party1 as party2 or
539
- # party3 which results in 50% of the cash to party1, 25% to party2, and 25% to party3. Passing a
540
- # number instead of an array will split the amount evenly (without loosing pennies when rounding).
580
+ # @param [Array<Numeric>, Numeric] parts how amount should be distributed to parts
541
581
  #
542
582
  # @return [Array<Money>]
543
583
  #
@@ -548,8 +588,8 @@ class Money
548
588
  # Money.new(100, "USD").allocate(3) #=> [Money.new(34), Money.new(33), Money.new(33)]
549
589
  #
550
590
  def allocate(parts)
551
- amounts = Money::Allocation.generate(fractional, parts, !Money.infinite_precision)
552
- amounts.map { |amount| self.class.new(amount, currency) }
591
+ amounts = Money::Allocation.generate(fractional, parts, !Money.default_infinite_precision)
592
+ amounts.map { |amount| dup_with(fractional: amount) }
553
593
  end
554
594
  alias_method :split, :allocate
555
595
 
@@ -566,16 +606,16 @@ class Money
566
606
  # Money.new(10.1, 'USD').round #=> Money.new(10, 'USD')
567
607
  #
568
608
  # @see
569
- # Money.infinite_precision
609
+ # Money.default_infinite_precision
570
610
  #
571
611
  def round(rounding_mode = self.class.rounding_mode, rounding_precision = 0)
572
612
  rounded_amount = as_d(@fractional).round(rounding_precision, rounding_mode)
573
- self.class.new(rounded_amount, currency, bank)
613
+ dup_with(fractional: rounded_amount)
574
614
  end
575
615
 
576
616
  # Creates a formatted price string according to several rules.
577
617
  #
578
- # @param [Hash] See Money::Formatter for the list of formatting options
618
+ # @param [Hash] rules See {Money::Formatter Money::Formatter} for the list of formatting options
579
619
  #
580
620
  # @return [String]
581
621
  #
@@ -601,6 +641,14 @@ class Money
601
641
  Money::Formatter::DEFAULTS[:decimal_mark]
602
642
  end
603
643
 
644
+ def dup_with(options = {})
645
+ self.class.new(
646
+ options[:fractional] || fractional,
647
+ options[:currency] || currency,
648
+ bank: options[:bank] || bank
649
+ )
650
+ end
651
+
604
652
  private
605
653
 
606
654
  def as_d(num)
@@ -612,7 +660,7 @@ class Money
612
660
  end
613
661
 
614
662
  def return_value(value)
615
- if self.class.infinite_precision
663
+ if self.class.default_infinite_precision
616
664
  value
617
665
  else
618
666
  value.round(0, self.class.rounding_mode).to_i
@@ -2,9 +2,9 @@
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 without losing pennies.
6
+ # The left-over pennies will be distributed round-robin amongst the parts. This means that
7
+ # parts listed first will likely receive more pennies than the ones listed later.
8
8
  #
9
9
  # The results should always add up to the original amount.
10
10
  #
@@ -13,7 +13,13 @@ class Money
13
13
  # Array<Numeric> — allocates the amounts proportionally to the given array
14
14
  #
15
15
  def self.generate(amount, parts, whole_amounts = true)
16
- parts = parts.is_a?(Numeric) ? Array.new(parts, 1) : parts.dup
16
+ parts = if parts.is_a?(Numeric)
17
+ Array.new(parts, 1)
18
+ elsif parts.all?(&:zero?)
19
+ Array.new(parts.count, 1)
20
+ else
21
+ parts.dup
22
+ end
17
23
 
18
24
  raise ArgumentError, 'need at least one party' if parts.empty?
19
25
 
@@ -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.