money 6.13.5 → 6.14.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.