money 6.13.4 → 6.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +26 -2
- data/README.md +29 -5
- data/config/currency_backwards_compatible.json +20 -0
- data/config/currency_iso.json +105 -12
- data/config/currency_non_iso.json +1 -0
- data/lib/money/bank/variable_exchange.rb +17 -7
- data/lib/money/currency.rb +9 -4
- data/lib/money/money.rb +124 -54
- data/lib/money/money/allocation.rb +10 -4
- data/lib/money/money/arithmetic.rb +18 -13
- data/lib/money/money/formatting_rules.rb +2 -0
- data/lib/money/rates_store/memory.rb +24 -23
- data/lib/money/version.rb +1 -1
- metadata +6 -7
@@ -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
|
data/lib/money/money.rb
CHANGED
@@ -91,51 +91,71 @@ class Money
|
|
91
91
|
class << self
|
92
92
|
|
93
93
|
# @!attribute [rw] default_bank
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
#
|
98
|
-
#
|
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
|
-
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
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
|
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
|
-
#
|
115
|
-
#
|
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]
|
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
|
-
#
|
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
|
-
#
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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.
|
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
|
-
|
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
|
-
|
198
|
-
with_rounding_mode(mode) { yield }
|
249
|
+
@rounding_mode
|
199
250
|
end
|
200
251
|
|
201
|
-
#
|
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
|
-
# @
|
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 [
|
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,
|
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
|
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 [
|
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,
|
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
|
-
|
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
|
513
|
-
# distributed round-robin amongst the parties. This means that
|
514
|
-
# receive more pennies than ones
|
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
|
-
#
|
517
|
-
#
|
518
|
-
# number instead of an array will split the amount evenly (without
|
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.
|
530
|
-
amounts.map { |amount|
|
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.
|
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
|
-
|
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.
|
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
|
6
|
-
# The left-over pennies will be distributed round-robin amongst the
|
7
|
-
#
|
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)
|
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
|
|