shopify-money 0.10.0

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.
@@ -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