money 6.13.4 → 6.14.0

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": ",",
@@ -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
@@ -91,51 +91,71 @@ 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.
121
+ #
122
+ # @return [Boolean]
116
123
  #
117
- # @!attribute [rw] infinite_precision
118
- # @return [Boolean] Use this to enable infinite precision cents
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
126
-
127
- # @attr_writer rounding_mode Use this to specify the rounding mode
129
+ # Used to specify precision for converting Rational to BigDecimal
128
130
  #
129
- # @!attribute default_currency
130
- # @return [Money::Currency] The default currency, which is used when
131
- # +Money.new+ is called without an explicit currency argument. The
132
- # default value is Currency.new("USD"). The value must be a valid
133
- # +Money::Currency+ instance.
134
- attr_writer :rounding_mode, :default_currency
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
135
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
136
145
  end
137
146
 
147
+ # @!attribute default_currency
148
+ # @return [Money::Currency] The default currency, which is used when
149
+ # +Money.new+ is called without an explicit currency argument. The
150
+ # default value is Currency.new("USD"). The value must be a valid
151
+ # +Money::Currency+ instance.
138
152
  def self.default_currency
153
+ if @using_deprecated_default_currency
154
+ warn '[WARNING] The default currency will change from `USD` to `nil` in the next major release. Make ' \
155
+ 'sure to set it explicitly using `Money.default_currency=` to avoid potential issues'
156
+ @using_deprecated_default_currency = false
157
+ end
158
+
139
159
  if @default_currency.respond_to?(:call)
140
160
  Money::Currency.new(@default_currency.call)
141
161
  else
@@ -143,10 +163,29 @@ class Money
143
163
  end
144
164
  end
145
165
 
166
+ def self.default_currency=(currency)
167
+ @using_deprecated_default_currency = false
168
+ @default_currency = currency
169
+ end
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
+
146
179
  def self.locale_backend=(value)
147
180
  @locale_backend = value ? LocaleBackend.find(value) : nil
148
181
  end
149
182
 
183
+ # @attr_writer rounding_mode Use this to specify the rounding mode
184
+ def self.rounding_mode=(new_rounding_mode)
185
+ @using_deprecated_default_rounding_mode = false
186
+ @rounding_mode = new_rounding_mode
187
+ end
188
+
150
189
  def self.use_i18n=(value)
151
190
  if value
152
191
  warn '[DEPRECATION] `use_i18n` is deprecated - use `Money.locale_backend = :i18n` instead for locale based formatting'
@@ -163,6 +202,7 @@ class Money
163
202
 
164
203
  # Set the default currency for creating new +Money+ object.
165
204
  self.default_currency = Currency.new("USD")
205
+ @using_deprecated_default_currency = true
166
206
 
167
207
  # Default to using i18n
168
208
  @use_i18n = true
@@ -171,10 +211,11 @@ class Money
171
211
  self.locale_backend = :legacy
172
212
 
173
213
  # Default to not using infinite precision cents
174
- self.infinite_precision = false
214
+ self.default_infinite_precision = false
175
215
 
176
216
  # Default to bankers rounding
177
217
  self.rounding_mode = BigDecimal::ROUND_HALF_EVEN
218
+ @using_deprecated_default_rounding_mode = true
178
219
 
179
220
  # Default the conversion of Rationals precision to 16
180
221
  self.conversion_precision = 16
@@ -192,18 +233,30 @@ class Money
192
233
  #
193
234
  # @return [BigDecimal::ROUND_MODE] rounding mode
194
235
  def self.rounding_mode(mode = nil)
195
- return Thread.current[:money_rounding_mode] || @rounding_mode unless mode
236
+ if mode
237
+ warn "[DEPRECATION] calling `rounding_mode` with a block is deprecated. Please use `.with_rounding_mode` instead."
238
+ return with_rounding_mode(mode) { yield }
239
+ end
240
+
241
+ return Thread.current[:money_rounding_mode] if Thread.current[:money_rounding_mode]
242
+
243
+ if @using_deprecated_default_rounding_mode
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
247
+ end
196
248
 
197
- warn "[DEPRECATION] calling `rounding_mode` with a block is deprecated. Please use `.with_rounding_mode` instead."
198
- with_rounding_mode(mode) { yield }
249
+ @rounding_mode
199
250
  end
200
251
 
201
- # This method temporarily changes the rounding mode. It will then return the
202
- # results of the block instead.
252
+ # Temporarily changes the rounding mode in a given block.
203
253
  #
204
254
  # @param [BigDecimal::ROUND_MODE] mode
205
255
  #
206
- # @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
207
260
  #
208
261
  # @example
209
262
  # fee = Money.with_rounding_mode(BigDecimal::ROUND_HALF_UP) do
@@ -241,7 +294,8 @@ class Money
241
294
  #
242
295
  # @param [Numeric] amount The numerical value of the money.
243
296
  # @param [Currency, String, Symbol] currency The currency format.
244
- # @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.
245
299
  #
246
300
  # @example
247
301
  # Money.from_amount(23.45, "USD") # => #<Money fractional:2345 currency:USD>
@@ -250,13 +304,12 @@ class Money
250
304
  # @return [Money]
251
305
  #
252
306
  # @see #initialize
253
- def self.from_amount(amount, currency = default_currency, bank = default_bank)
307
+ def self.from_amount(amount, currency = default_currency, options = {})
254
308
  raise ArgumentError, "'amount' must be numeric" unless Numeric === amount
255
309
 
256
310
  currency = Currency.wrap(currency) || Money.default_currency
257
311
  value = amount.to_d * currency.subunit_to_unit
258
- value = value.round(0, rounding_mode) unless infinite_precision
259
- new(value, currency, bank)
312
+ new(value, currency, options)
260
313
  end
261
314
 
262
315
  # Creates a new Money object of value given in the
@@ -270,7 +323,8 @@ class Money
270
323
  # argument, a Money will be created in that currency with fractional value
271
324
  # = 0.
272
325
  # @param [Currency, String, Symbol] currency The currency format.
273
- # @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.
274
328
  #
275
329
  # @return [Money]
276
330
  #
@@ -279,11 +333,17 @@ class Money
279
333
  # Money.new(100, "USD") #=> #<Money @fractional=100 @currency="USD">
280
334
  # Money.new(100, "EUR") #=> #<Money @fractional=100 @currency="EUR">
281
335
  #
282
- 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
+
283
342
  @fractional = as_d(obj.respond_to?(:fractional) ? obj.fractional : obj)
284
343
  @currency = obj.respond_to?(:currency) ? obj.currency : Currency.wrap(currency)
285
344
  @currency ||= Money.default_currency
286
- @bank = obj.respond_to?(:bank) ? obj.bank : bank
345
+ @bank = obj.respond_to?(:bank) ? obj.bank : options[:bank]
346
+ @bank ||= Money.default_bank
287
347
 
288
348
  # BigDecimal can be Infinity and NaN, money of that amount does not make sense
289
349
  raise ArgumentError, 'must be initialized with a finite value' unless @fractional.finite?
@@ -432,7 +492,7 @@ class Money
432
492
  if !new_currency || currency == new_currency
433
493
  self
434
494
  else
435
- self.class.new(fractional, new_currency, bank)
495
+ dup_with(currency: new_currency)
436
496
  end
437
497
  end
438
498
 
@@ -509,13 +569,15 @@ class Money
509
569
  exchange_to("EUR")
510
570
  end
511
571
 
512
- # Splits a given amount in parts without loosing pennies. The left-over pennies will be
513
- # distributed round-robin amongst the parties. This means that parties listed first will likely
514
- # 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.
515
575
  #
516
- # @param [Array<Numeric>, Numeric] pass [2, 1, 1] to give twice as much to party1 as party2 or
517
- # party3 which results in 50% of the cash to party1, 25% to party2, and 25% to party3. Passing a
518
- # number instead of an array will split the amount evenly (without loosing pennies when rounding).
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).
579
+ #
580
+ # @param [Array<Numeric>, Numeric] parts how amount should be distributed to parts
519
581
  #
520
582
  # @return [Array<Money>]
521
583
  #
@@ -526,8 +588,8 @@ class Money
526
588
  # Money.new(100, "USD").allocate(3) #=> [Money.new(34), Money.new(33), Money.new(33)]
527
589
  #
528
590
  def allocate(parts)
529
- amounts = Money::Allocation.generate(fractional, parts, !Money.infinite_precision)
530
- 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) }
531
593
  end
532
594
  alias_method :split, :allocate
533
595
 
@@ -544,16 +606,16 @@ class Money
544
606
  # Money.new(10.1, 'USD').round #=> Money.new(10, 'USD')
545
607
  #
546
608
  # @see
547
- # Money.infinite_precision
609
+ # Money.default_infinite_precision
548
610
  #
549
611
  def round(rounding_mode = self.class.rounding_mode, rounding_precision = 0)
550
612
  rounded_amount = as_d(@fractional).round(rounding_precision, rounding_mode)
551
- self.class.new(rounded_amount, currency, bank)
613
+ dup_with(fractional: rounded_amount)
552
614
  end
553
615
 
554
616
  # Creates a formatted price string according to several rules.
555
617
  #
556
- # @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
557
619
  #
558
620
  # @return [String]
559
621
  #
@@ -579,6 +641,14 @@ class Money
579
641
  Money::Formatter::DEFAULTS[:decimal_mark]
580
642
  end
581
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
+
582
652
  private
583
653
 
584
654
  def as_d(num)
@@ -590,7 +660,7 @@ class Money
590
660
  end
591
661
 
592
662
  def return_value(value)
593
- if self.class.infinite_precision
663
+ if self.class.default_infinite_precision
594
664
  value
595
665
  else
596
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