shopify-money 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,82 @@
1
+ {
2
+ "jep": {
3
+ "priority": 100,
4
+ "iso_code": "JEP",
5
+ "name": "Jersey Pound",
6
+ "symbol": "£",
7
+ "disambiguate_symbol": "JEP",
8
+ "alternate_symbols": [],
9
+ "subunit": "Penny",
10
+ "subunit_to_unit": 100,
11
+ "symbol_first": true,
12
+ "html_entity": "£",
13
+ "decimal_mark": ".",
14
+ "thousands_separator": ",",
15
+ "iso_numeric": "",
16
+ "smallest_denomination": 1
17
+ },
18
+ "ggp": {
19
+ "priority": 100,
20
+ "iso_code": "GGP",
21
+ "name": "Guernsey Pound",
22
+ "symbol": "£",
23
+ "disambiguate_symbol": "GGP",
24
+ "alternate_symbols": [],
25
+ "subunit": "Penny",
26
+ "subunit_to_unit": 100,
27
+ "symbol_first": true,
28
+ "html_entity": "£",
29
+ "decimal_mark": ".",
30
+ "thousands_separator": ",",
31
+ "iso_numeric": "",
32
+ "smallest_denomination": 1
33
+ },
34
+ "imp": {
35
+ "priority": 100,
36
+ "iso_code": "IMP",
37
+ "name": "Isle of Man Pound",
38
+ "symbol": "£",
39
+ "disambiguate_symbol": "IMP",
40
+ "alternate_symbols": ["M£"],
41
+ "subunit": "Penny",
42
+ "subunit_to_unit": 100,
43
+ "symbol_first": true,
44
+ "html_entity": "£",
45
+ "decimal_mark": ".",
46
+ "thousands_separator": ",",
47
+ "iso_numeric": "",
48
+ "smallest_denomination": 1
49
+ },
50
+ "xfu": {
51
+ "priority": 100,
52
+ "iso_code": "XFU",
53
+ "name": "UIC Franc",
54
+ "symbol": "",
55
+ "disambiguate_symbol": "XFU",
56
+ "alternate_symbols": [],
57
+ "subunit": "",
58
+ "subunit_to_unit": 100,
59
+ "symbol_first": true,
60
+ "html_entity": "",
61
+ "decimal_mark": ".",
62
+ "thousands_separator": ",",
63
+ "iso_numeric": "",
64
+ "smallest_denomination": ""
65
+ },
66
+ "gbx": {
67
+ "priority": 100,
68
+ "iso_code": "GBX",
69
+ "name": "British Penny",
70
+ "symbol": "",
71
+ "disambiguate_symbol": "GBX",
72
+ "alternate_symbols": [],
73
+ "subunit": "",
74
+ "subunit_to_unit": 1,
75
+ "symbol_first": true,
76
+ "html_entity": "",
77
+ "decimal_mark": ".",
78
+ "thousands_separator": ",",
79
+ "iso_numeric": "",
80
+ "smallest_denomination": 1
81
+ }
82
+ }
data/dev.yml ADDED
@@ -0,0 +1,9 @@
1
+ # For internal use, but the requirements below
2
+ # describe the required dependencies
3
+ ---
4
+ name: money
5
+ up:
6
+ - ruby: 2.3.3
7
+ - bundler
8
+ commands:
9
+ test: bundle exec rspec
data/lib/money.rb ADDED
@@ -0,0 +1,10 @@
1
+ require_relative 'money/money_parser'
2
+ require_relative 'money/helpers'
3
+ require_relative 'money/currency'
4
+ require_relative 'money/null_currency'
5
+ require_relative 'money/money'
6
+ require_relative 'money/deprecations'
7
+ require_relative 'money/accounting_money_parser'
8
+ require_relative 'money/core_extensions'
9
+ require_relative 'money_accessor'
10
+ require_relative 'money_column' if defined?(ActiveRecord)
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AccountingMoneyParser < MoneyParser
4
+ def parse(input, currency = nil, **options)
5
+ # set () to mean negativity. ignore $
6
+ super(input.gsub(/\(\$?(.*?)\)/, '-\1'), currency, **options)
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ # Allows Writing of 100.to_money for +Numeric+ types
2
+ # 100.to_money => #<Money @cents=10000>
3
+ # 100.37.to_money => #<Money @cents=10037>
4
+ class Numeric
5
+ def to_money(currency = nil)
6
+ Money.new(self, currency)
7
+ end
8
+ end
9
+
10
+ # Allows Writing of '100'.to_money for +String+ types
11
+ # Excess characters will be discarded
12
+ # '100'.to_money => #<Money @cents=10000>
13
+ # '100.37'.to_money => #<Money @cents=10037>
14
+ class String
15
+ def to_money(currency = nil)
16
+ empty? ? Money.empty : Money.parse(self, currency)
17
+ end
18
+ end
@@ -0,0 +1,59 @@
1
+ require "money/currency/loader"
2
+
3
+ class Money
4
+ class Currency
5
+ @@mutex = Mutex.new
6
+ @@loaded_currencies = {}
7
+
8
+ class UnknownCurrency < ArgumentError; end
9
+
10
+ class << self
11
+ def new(currency_iso)
12
+ raise UnknownCurrency, "Currency can't be blank" if currency_iso.nil? || currency_iso.to_s.empty?
13
+ iso = currency_iso.to_s.downcase
14
+ @@loaded_currencies[iso] || @@mutex.synchronize { @@loaded_currencies[iso] = super(iso) }
15
+ end
16
+ alias_method :find!, :new
17
+
18
+ def find(currency_iso)
19
+ new(currency_iso)
20
+ rescue UnknownCurrency
21
+ nil
22
+ end
23
+
24
+ def currencies
25
+ @@currencies ||= Loader.load_currencies
26
+ end
27
+ end
28
+
29
+ attr_reader :iso_code, :iso_numeric, :name, :smallest_denomination, :subunit_symbol,
30
+ :subunit_to_unit, :minor_units, :symbol, :disambiguate_symbol, :decimal_mark
31
+
32
+ def initialize(currency_iso)
33
+ data = self.class.currencies[currency_iso]
34
+ raise UnknownCurrency, "Invalid iso4217 currency '#{currency_iso}'" unless data
35
+ @symbol = data['symbol']
36
+ @disambiguate_symbol = data['disambiguate_symbol'] || data['symbol']
37
+ @subunit_symbol = data['subunit_symbol']
38
+ @iso_code = data['iso_code']
39
+ @iso_numeric = data['iso_numeric']
40
+ @name = data['name']
41
+ @smallest_denomination = data['smallest_denomination']
42
+ @subunit_to_unit = data['subunit_to_unit']
43
+ @decimal_mark = data['decimal_mark']
44
+ @minor_units = subunit_to_unit == 0 ? 0 : Math.log(subunit_to_unit, 10).round.to_i
45
+ freeze
46
+ end
47
+
48
+ def eql?(other)
49
+ self.class == other.class && iso_code == other.iso_code
50
+ end
51
+
52
+ def compatible?(other)
53
+ other.is_a?(NullCurrency) || eql?(other)
54
+ end
55
+
56
+ alias_method :==, :eql?
57
+ alias_method :to_s, :iso_code
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ require 'json'
2
+
3
+ class Money
4
+ class Currency
5
+ module Loader
6
+ extend self
7
+
8
+ CURRENCY_DATA_PATH = File.expand_path("../../../../config", __FILE__)
9
+
10
+ def load_currencies
11
+ currencies = {}
12
+ currencies.merge! parse_currency_file("currency_historic.json")
13
+ currencies.merge! parse_currency_file("currency_non_iso.json")
14
+ currencies.merge! parse_currency_file("currency_iso.json")
15
+ end
16
+
17
+ private
18
+
19
+ def parse_currency_file(filename)
20
+ json = File.read("#{CURRENCY_DATA_PATH}/#{filename}")
21
+ json.force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
22
+ JSON.parse(json)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ Money.class_eval do
2
+ ACTIVE_SUPPORT_DEFINED = defined?(ActiveSupport)
3
+
4
+ def self.active_support_deprecator
5
+ @active_support_deprecator ||= ActiveSupport::Deprecation.new('1.0.0', 'Shopify/Money')
6
+ end
7
+
8
+ def self.deprecate(message)
9
+ if ACTIVE_SUPPORT_DEFINED
10
+ external_callstack = caller_locations.reject do |location|
11
+ location.to_s.include?('gems/money')
12
+ end
13
+ active_support_deprecator.warn("[Shopify/Money] #{message}\n", external_callstack)
14
+ else
15
+ Kernel.warn("DEPRECATION WARNING: [Shopify/Money] #{message}\n")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+ require 'bigdecimal'
3
+
4
+ class Money
5
+ module Helpers
6
+ module_function
7
+
8
+ NUMERIC_REGEX = /\A\s*[\+\-]?\d*(\.\d+)?\s*\z/
9
+ DECIMAL_ZERO = BigDecimal.new(0).freeze
10
+ MAX_DECIMAL = 21
11
+
12
+ def value_to_decimal(num)
13
+ value =
14
+ case num
15
+ when Money
16
+ num.value
17
+ when BigDecimal
18
+ num
19
+ when nil, 0, ''
20
+ DECIMAL_ZERO
21
+ when Integer
22
+ BigDecimal.new(num)
23
+ when Float
24
+ BigDecimal.new(num, Float::DIG)
25
+ when Rational
26
+ BigDecimal.new(num, MAX_DECIMAL)
27
+ when String
28
+ string_to_decimal(num)
29
+ else
30
+ raise ArgumentError, "could not parse as decimal #{num.inspect}"
31
+ end
32
+ return DECIMAL_ZERO if value.sign == BigDecimal::SIGN_NEGATIVE_ZERO
33
+ value
34
+ end
35
+
36
+ def value_to_currency(currency)
37
+ case currency
38
+ when Money::Currency, Money::NullCurrency
39
+ currency
40
+ when nil, ''
41
+ default = Money.current_currency || Money.default_currency
42
+ raise(ArgumentError, 'missing currency') if default.nil? || default == ''
43
+ value_to_currency(default)
44
+ when 'xxx', 'XXX'
45
+ Money::NULL_CURRENCY
46
+ when String
47
+ begin
48
+ Currency.find!(currency)
49
+ rescue Money::Currency::UnknownCurrency => error
50
+ Money.deprecate(error.message)
51
+ Money::NULL_CURRENCY
52
+ end
53
+ else
54
+ raise ArgumentError, "could not parse as currency #{currency.inspect}"
55
+ end
56
+ end
57
+
58
+ def string_to_decimal(num)
59
+ if num =~ NUMERIC_REGEX
60
+ return BigDecimal.new(num)
61
+ end
62
+
63
+ Money.deprecate("using Money.new('#{num}') is deprecated and will raise an ArgumentError in the next major release")
64
+ begin
65
+ BigDecimal.new(num)
66
+ rescue ArgumentError
67
+ DECIMAL_ZERO
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,408 @@
1
+ class Money
2
+ include Comparable
3
+ extend Forwardable
4
+
5
+ NULL_CURRENCY = NullCurrency.new.freeze
6
+
7
+ attr_reader :value, :currency
8
+ def_delegators :@value, :zero?, :nonzero?, :positive?, :negative?, :to_i, :to_f, :hash
9
+
10
+ class << self
11
+ attr_accessor :parser, :default_currency
12
+
13
+ def new(value = 0, currency = nil)
14
+ value = Helpers.value_to_decimal(value)
15
+ currency = Helpers.value_to_currency(currency)
16
+
17
+ if value.zero?
18
+ @@zero_money ||= {}
19
+ @@zero_money[currency.iso_code] ||= super(Helpers::DECIMAL_ZERO, currency)
20
+ else
21
+ super(value, currency)
22
+ end
23
+ end
24
+ alias_method :from_amount, :new
25
+
26
+ def zero
27
+ new(0, NULL_CURRENCY)
28
+ end
29
+ alias_method :empty, :zero
30
+
31
+ def parse(*args)
32
+ parser.parse(*args)
33
+ end
34
+
35
+ def from_cents(cents, currency = nil)
36
+ new(cents.round.to_f / 100, currency)
37
+ end
38
+
39
+ def from_subunits(subunits, currency_iso)
40
+ currency = Helpers.value_to_currency(currency_iso)
41
+ value = Helpers.value_to_decimal(subunits) / currency.subunit_to_unit
42
+ new(value, currency)
43
+ end
44
+
45
+ def rational(money1, money2)
46
+ money1.send(:arithmetic, money2) do
47
+ factor = money1.currency.subunit_to_unit * money2.currency.subunit_to_unit
48
+ Rational((money1.value * factor).to_i, (money2.value * factor).to_i)
49
+ end
50
+ end
51
+
52
+ def current_currency
53
+ Thread.current[:money_currency]
54
+ end
55
+
56
+ def current_currency=(currency)
57
+ Thread.current[:money_currency] = currency
58
+ end
59
+
60
+ # Set Money.default_currency inside the supplied block, resets it to
61
+ # the previous value when done to prevent leaking state. Similar to
62
+ # I18n.with_locale and ActiveSupport's Time.use_zone. This won't affect
63
+ # instances being created with explicitly set currency.
64
+ def with_currency(new_currency)
65
+ begin
66
+ old_currency = Money.current_currency
67
+ Money.current_currency = new_currency
68
+ yield
69
+ ensure
70
+ Money.current_currency = old_currency
71
+ end
72
+ end
73
+
74
+ def default_settings
75
+ self.parser = MoneyParser
76
+ self.default_currency = Money::NULL_CURRENCY
77
+ end
78
+ end
79
+ default_settings
80
+
81
+ def initialize(value, currency)
82
+ raise ArgumentError if value.nan?
83
+ @currency = Helpers.value_to_currency(currency)
84
+ @value = value.round(@currency.minor_units)
85
+ freeze
86
+ end
87
+
88
+ def init_with(coder)
89
+ initialize(Helpers.value_to_decimal(coder['value']), coder['currency'])
90
+ end
91
+
92
+ def encode_with(coder)
93
+ coder['value'] = @value.to_s('F')
94
+ coder['currency'] = @currency.iso_code
95
+ end
96
+
97
+ def cents
98
+ # Money.deprecate('`money.cents` is deprecated and will be removed in the next major release. Please use `money.subunits` instead. Keep in mind, subunits are currency aware.')
99
+ (value * 100).to_i
100
+ end
101
+
102
+ def subunits
103
+ (@value * @currency.subunit_to_unit).to_i
104
+ end
105
+
106
+ def no_currency?
107
+ currency.is_a?(NullCurrency)
108
+ end
109
+
110
+ def -@
111
+ Money.new(-value, currency)
112
+ end
113
+
114
+ def <=>(other)
115
+ arithmetic(other) do |money|
116
+ value <=> money.value
117
+ end
118
+ end
119
+
120
+ def +(other)
121
+ arithmetic(other) do |money|
122
+ Money.new(value + money.value, calculated_currency(money.currency))
123
+ end
124
+ end
125
+
126
+ def -(other)
127
+ arithmetic(other) do |money|
128
+ Money.new(value - money.value, calculated_currency(money.currency))
129
+ end
130
+ end
131
+
132
+ def *(numeric)
133
+ unless numeric.is_a?(Numeric)
134
+ Money.deprecate("Multiplying Money with #{numeric.class.name} is deprecated and will be removed in the next major release.")
135
+ end
136
+ Money.new(value.to_r * numeric, currency)
137
+ end
138
+
139
+ def /(numeric)
140
+ raise "[Money] Dividing money objects can lose pennies. Use #split instead"
141
+ end
142
+
143
+ def inspect
144
+ "#<#{self.class} value:#{self} currency:#{self.currency}>"
145
+ end
146
+
147
+ def ==(other)
148
+ eql?(other)
149
+ end
150
+
151
+ def eql?(other)
152
+ return false unless other.is_a?(Money)
153
+ return false unless currency.compatible?(other.currency)
154
+ value == other.value
155
+ end
156
+
157
+ class ReverseOperationProxy
158
+ include Comparable
159
+
160
+ def initialize(value)
161
+ @value = value
162
+ end
163
+
164
+ def <=>(other)
165
+ -(other <=> @value)
166
+ end
167
+
168
+ def +(other)
169
+ other + @value
170
+ end
171
+
172
+ def -(other)
173
+ -(other - @value)
174
+ end
175
+
176
+ def *(other)
177
+ other * @value
178
+ end
179
+ end
180
+
181
+ def coerce(other)
182
+ raise TypeError, "Money can't be coerced into #{other.class}" unless other.is_a?(Numeric)
183
+ [ReverseOperationProxy.new(other), self]
184
+ end
185
+
186
+ def to_money(_currency = nil)
187
+ self
188
+ end
189
+
190
+ def to_d
191
+ value
192
+ end
193
+
194
+ def to_s(style = nil)
195
+ case style
196
+ when :legacy_dollars
197
+ sprintf("%.2f", value)
198
+ when :amount, nil
199
+ sprintf("%.#{currency.minor_units}f", value)
200
+ end
201
+ end
202
+
203
+ def to_liquid
204
+ cents
205
+ end
206
+
207
+ def to_json(options = {})
208
+ to_s
209
+ end
210
+
211
+ def as_json(*args)
212
+ to_s
213
+ end
214
+
215
+ def abs
216
+ Money.new(value.abs, currency)
217
+ end
218
+
219
+ def floor
220
+ Money.new(value.floor, currency)
221
+ end
222
+
223
+ def round(ndigits=0)
224
+ Money.new(value.round(ndigits), currency)
225
+ end
226
+
227
+ def fraction(rate)
228
+ raise ArgumentError, "rate should be positive" if rate < 0
229
+
230
+ result = value / (1 + rate)
231
+ Money.new(result, currency)
232
+ end
233
+
234
+ # Allocates money between different parties without losing pennies.
235
+ # After the mathematically split has been performed, left over pennies will
236
+ # be distributed round-robin amongst the parties. This means that parties
237
+ # listed first will likely receive more pennies than ones that are listed later
238
+ #
239
+ # @param splits [Array<Numeric>]
240
+ # @return [Array<Money>]
241
+ #
242
+ # @example
243
+ # Money.new(5, "USD").allocate([0.50, 0.25, 0.25])
244
+ # #=> [#<Money value:2.50 currency:USD>, #<Money value:1.25 currency:USD>, #<Money value:1.25 currency:USD>]
245
+ # Money.new(5, "USD").allocate([0.3, 0.7])
246
+ # #=> [#<Money value:1.50 currency:USD>, #<Money value:3.50 currency:USD>]
247
+ # Money.new(100, "USD").allocate([0.33, 0.33, 0.33])
248
+ # #=> [#<Money value:33.34 currency:USD>, #<Money value:33.33 currency:USD>, #<Money value:33.33 currency:USD>]
249
+
250
+ # @example left over cents distributed to first party due to rounding, and two solutions for a more natural distribution
251
+ # Money.new(30, "USD").allocate([0.667, 0.333])
252
+ # #=> [#<Money value:20.01 currency:USD>, #<Money value:9.99 currency:USD>]
253
+ # Money.new(30, "USD").allocate([0.333, 0.667])
254
+ # #=> [#<Money value:20.00 currency:USD>, #<Money value:10.00 currency:USD>]
255
+ # Money.new(30, "USD").allocate([Rational(2, 3), Rational(1, 3)])
256
+ # #=> [#<Money value:20.00 currency:USD>, #<Money value:10.00 currency:USD>]
257
+ def allocate(splits)
258
+ if all_rational?(splits)
259
+ allocations = splits.inject(0) { |sum, n| sum + n }
260
+ else
261
+ allocations = splits.inject(0) { |sum, n| sum + Helpers.value_to_decimal(n) }
262
+ end
263
+
264
+ if (allocations - BigDecimal("1")) > Float::EPSILON
265
+ raise ArgumentError, "splits add to more than 100%"
266
+ end
267
+
268
+ amounts, left_over = amounts_from_splits(allocations, splits)
269
+
270
+ left_over.to_i.times { |i| amounts[i % amounts.length] += 1 }
271
+
272
+ amounts.collect { |subunits| Money.from_subunits(subunits, currency) }
273
+ end
274
+
275
+ # Allocates money between different parties up to the maximum amounts specified.
276
+ # Left over pennies will be assigned round-robin up to the maximum specified.
277
+ # Pennies are dropped when the maximums are attained.
278
+ #
279
+ # @example
280
+ # Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.75)])
281
+ # #=> [Money.new(26), Money.new(4.75)]
282
+ #
283
+ # Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.74)]
284
+ # #=> [Money.new(26), Money.new(4.74)]
285
+ #
286
+ # Money.new(30).allocate_max_amounts([Money.new(15), Money.new(15)]
287
+ # #=> [Money.new(15), Money.new(15)]
288
+ #
289
+ # Money.new(1).allocate_max_amounts([Money.new(33), Money.new(33), Money.new(33)])
290
+ # #=> [Money.new(0.34), Money.new(0.33), Money.new(0.33)]
291
+ #
292
+ # Money.new(100).allocate_max_amounts([Money.new(5), Money.new(2)])
293
+ # #=> [Money.new(5), Money.new(2)]
294
+ def allocate_max_amounts(maximums)
295
+ allocation_currency = extract_currency(maximums + [self])
296
+ maximums = maximums.map { |max| max.to_money(allocation_currency) }
297
+ maximums_total = maximums.reduce(Money.new(0, allocation_currency), :+)
298
+
299
+ splits = maximums.map do |max_amount|
300
+ next(0) if maximums_total.zero?
301
+ Money.rational(max_amount, maximums_total)
302
+ end
303
+
304
+ total_allocatable = [
305
+ value * allocation_currency.subunit_to_unit,
306
+ maximums_total.value * allocation_currency.subunit_to_unit
307
+ ].min
308
+
309
+ subunits_amounts, left_over = amounts_from_splits(1, splits, total_allocatable)
310
+
311
+ subunits_amounts.each_with_index do |amount, index|
312
+ break unless left_over > 0
313
+
314
+ max_amount = maximums[index].value * allocation_currency.subunit_to_unit
315
+ next unless amount < max_amount
316
+
317
+ left_over -= 1
318
+ subunits_amounts[index] += 1
319
+ end
320
+
321
+ subunits_amounts.map { |cents| Money.from_subunits(cents, allocation_currency) }
322
+ end
323
+
324
+ # Split money amongst parties evenly without losing pennies.
325
+ #
326
+ # @param [2] number of parties.
327
+ #
328
+ # @return [Array<Money, Money, Money>]
329
+ #
330
+ # @example
331
+ # Money.new(100, "USD").split(3) #=> [Money.new(34), Money.new(33), Money.new(33)]
332
+ def split(num)
333
+ raise ArgumentError, "need at least one party" if num < 1
334
+ subunits = self.subunits
335
+ low = Money.from_subunits(subunits / num, currency)
336
+ high = Money.from_subunits(low.subunits + 1, currency)
337
+
338
+ remainder = subunits % num
339
+ result = []
340
+
341
+ num.times do |index|
342
+ result[index] = index < remainder ? high : low
343
+ end
344
+
345
+ return result
346
+ end
347
+
348
+ # Clamps the value to be within the specified minimum and maximum. Returns
349
+ # self if the value is within bounds, otherwise a new Money object with the
350
+ # closest min or max value.
351
+ #
352
+ # @example
353
+ # Money.new(50, "CAD").clamp(1, 100) #=> Money.new(50, "CAD")
354
+ #
355
+ # Money.new(120, "CAD").clamp(0, 100) #=> Money.new(100, "CAD")
356
+ def clamp(min, max)
357
+ raise ArgumentError, 'min cannot be greater than max' if min > max
358
+
359
+ clamped_value = min if self.value < min
360
+ clamped_value = max if self.value > max
361
+
362
+ if clamped_value.nil?
363
+ self
364
+ else
365
+ Money.new(clamped_value, self.currency)
366
+ end
367
+ end
368
+
369
+ private
370
+
371
+ def all_rational?(splits)
372
+ splits.all? { |split| split.is_a?(Rational) }
373
+ end
374
+
375
+ def amounts_from_splits(allocations, splits, subunits_to_split = subunits)
376
+ left_over = subunits_to_split
377
+
378
+ amounts = splits.collect do |ratio|
379
+ frac = (Helpers.value_to_decimal(subunits_to_split * ratio) / allocations).floor
380
+ left_over -= frac
381
+ frac
382
+ end
383
+
384
+ [amounts, left_over]
385
+ end
386
+
387
+ def arithmetic(money_or_numeric)
388
+ raise TypeError, "#{money_or_numeric.class.name} can't be coerced into Money" unless money_or_numeric.respond_to?(:to_money)
389
+ other = money_or_numeric.to_money(currency)
390
+
391
+ unless currency.compatible?(other.currency)
392
+ Money.deprecate("mathematical operation not permitted for Money objects with different currencies #{other.currency} and #{currency}.")
393
+ end
394
+ yield(other)
395
+ end
396
+
397
+ def calculated_currency(other)
398
+ no_currency? ? other : currency
399
+ end
400
+
401
+ def extract_currency(money_array)
402
+ currencies = money_array.lazy.select { |money| money.is_a?(Money) }.reject(&:no_currency?).map(&:currency).to_a.uniq
403
+ if currencies.size > 1
404
+ raise ArgumentError, "operation not permitted for Money objects with different currencies #{currencies.join(', ')}"
405
+ end
406
+ currencies.first || NULL_CURRENCY
407
+ end
408
+ end