shopify-money 0.12.0 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/money.rb +1 -0
- data/lib/money/allocator.rb +145 -0
- data/lib/money/money.rb +5 -109
- data/lib/money/version.rb +1 -1
- data/spec/allocator_spec.rb +135 -0
- data/spec/money_spec.rb +10 -94
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca42130a743b47dba4be201df379a894d92e9e6101338fd41c6ad84b94805997
|
4
|
+
data.tar.gz: 4b4749500c63bdb88d8f63f36628a845f355aad3f572e428dd60804ad107b591
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9bffc191b4fea246018814cd3961b5bd298212d076c6f6eb6b82d8c19bd377387f360b1a982dc1978ae8a29f531dd57e7127d47f5f4192b0fe5246a8015bc7f2
|
7
|
+
data.tar.gz: d99c2ca80783732eda61cb4e064140131b0aee8da9f56b541e9e5fad7cd1fa4c9ec7362c9fb343ae0c2716888135a38f76799435727f91f025ccddeede94aba4
|
data/lib/money.rb
CHANGED
@@ -2,6 +2,7 @@ require_relative 'money/money_parser'
|
|
2
2
|
require_relative 'money/helpers'
|
3
3
|
require_relative 'money/currency'
|
4
4
|
require_relative 'money/null_currency'
|
5
|
+
require_relative 'money/allocator'
|
5
6
|
require_relative 'money/money'
|
6
7
|
require_relative 'money/errors'
|
7
8
|
require_relative 'money/deprecations'
|
@@ -0,0 +1,145 @@
|
|
1
|
+
class Money
|
2
|
+
class Allocator < SimpleDelegator
|
3
|
+
def initialize(money)
|
4
|
+
super
|
5
|
+
end
|
6
|
+
|
7
|
+
# Allocates money between different parties without losing pennies.
|
8
|
+
# After the mathematically split has been performed, left over pennies will
|
9
|
+
# be distributed round-robin amongst the parties. This means that parties
|
10
|
+
# listed first will likely receive more pennies than ones that are listed later
|
11
|
+
#
|
12
|
+
# @param splits [Array<Numeric>]
|
13
|
+
# @param strategy Symbol
|
14
|
+
# @return [Array<Money>]
|
15
|
+
#
|
16
|
+
# Strategies:
|
17
|
+
# - `:roundrobin` (default): leftover pennies will be accumulated starting from the first allocation left to right
|
18
|
+
# - `:roundrobin_reverse`: leftover pennies will be accumulated starting from the last allocation right to left
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# Money.new(5, "USD").allocate([0.50, 0.25, 0.25])
|
22
|
+
# #=> [#<Money value:2.50 currency:USD>, #<Money value:1.25 currency:USD>, #<Money value:1.25 currency:USD>]
|
23
|
+
# Money.new(5, "USD").allocate([0.3, 0.7])
|
24
|
+
# #=> [#<Money value:1.50 currency:USD>, #<Money value:3.50 currency:USD>]
|
25
|
+
# Money.new(100, "USD").allocate([0.33, 0.33, 0.33])
|
26
|
+
# #=> [#<Money value:33.34 currency:USD>, #<Money value:33.33 currency:USD>, #<Money value:33.33 currency:USD>]
|
27
|
+
|
28
|
+
# @example left over cents distributed to first party due to rounding, and two solutions for a more natural distribution
|
29
|
+
# Money.new(30, "USD").allocate([0.667, 0.333])
|
30
|
+
# #=> [#<Money value:20.01 currency:USD>, #<Money value:9.99 currency:USD>]
|
31
|
+
# Money.new(30, "USD").allocate([0.333, 0.667])
|
32
|
+
# #=> [#<Money value:20.00 currency:USD>, #<Money value:10.00 currency:USD>]
|
33
|
+
# Money.new(30, "USD").allocate([Rational(2, 3), Rational(1, 3)])
|
34
|
+
# #=> [#<Money value:20.00 currency:USD>, #<Money value:10.00 currency:USD>]
|
35
|
+
|
36
|
+
# @example left over pennies distributed reverse order when using roundrobin_reverse strategy
|
37
|
+
# Money.new(10.01, "USD").allocate([0.5, 0.5], :roundrobin_reverse)
|
38
|
+
# #=> [#<Money value:5.00 currency:USD>, #<Money value:5.01 currency:USD>]
|
39
|
+
def allocate(splits, strategy = :roundrobin)
|
40
|
+
if all_rational?(splits)
|
41
|
+
allocations = splits.inject(0) { |sum, n| sum + n }
|
42
|
+
else
|
43
|
+
allocations = splits.inject(0) { |sum, n| sum + Helpers.value_to_decimal(n) }
|
44
|
+
end
|
45
|
+
|
46
|
+
if (allocations - BigDecimal("1")) > Float::EPSILON
|
47
|
+
raise ArgumentError, "splits add to more than 100%"
|
48
|
+
end
|
49
|
+
|
50
|
+
amounts, left_over = amounts_from_splits(allocations, splits)
|
51
|
+
|
52
|
+
left_over.to_i.times do |i|
|
53
|
+
amounts[allocation_index_for(strategy, amounts.length, i)] += 1
|
54
|
+
end
|
55
|
+
|
56
|
+
amounts.collect { |subunits| Money.from_subunits(subunits, currency) }
|
57
|
+
end
|
58
|
+
|
59
|
+
# Allocates money between different parties up to the maximum amounts specified.
|
60
|
+
# Left over pennies will be assigned round-robin up to the maximum specified.
|
61
|
+
# Pennies are dropped when the maximums are attained.
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.75)])
|
65
|
+
# #=> [Money.new(26), Money.new(4.75)]
|
66
|
+
#
|
67
|
+
# Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.74)]
|
68
|
+
# #=> [Money.new(26), Money.new(4.74)]
|
69
|
+
#
|
70
|
+
# Money.new(30).allocate_max_amounts([Money.new(15), Money.new(15)]
|
71
|
+
# #=> [Money.new(15), Money.new(15)]
|
72
|
+
#
|
73
|
+
# Money.new(1).allocate_max_amounts([Money.new(33), Money.new(33), Money.new(33)])
|
74
|
+
# #=> [Money.new(0.34), Money.new(0.33), Money.new(0.33)]
|
75
|
+
#
|
76
|
+
# Money.new(100).allocate_max_amounts([Money.new(5), Money.new(2)])
|
77
|
+
# #=> [Money.new(5), Money.new(2)]
|
78
|
+
def allocate_max_amounts(maximums)
|
79
|
+
allocation_currency = extract_currency(maximums + [self.__getobj__])
|
80
|
+
maximums = maximums.map { |max| max.to_money(allocation_currency) }
|
81
|
+
maximums_total = maximums.reduce(Money.new(0, allocation_currency), :+)
|
82
|
+
|
83
|
+
splits = maximums.map do |max_amount|
|
84
|
+
next(0) if maximums_total.zero?
|
85
|
+
Money.rational(max_amount, maximums_total)
|
86
|
+
end
|
87
|
+
|
88
|
+
total_allocatable = [
|
89
|
+
value * allocation_currency.subunit_to_unit,
|
90
|
+
maximums_total.value * allocation_currency.subunit_to_unit
|
91
|
+
].min
|
92
|
+
|
93
|
+
subunits_amounts, left_over = amounts_from_splits(1, splits, total_allocatable)
|
94
|
+
|
95
|
+
subunits_amounts.each_with_index do |amount, index|
|
96
|
+
break unless left_over > 0
|
97
|
+
|
98
|
+
max_amount = maximums[index].value * allocation_currency.subunit_to_unit
|
99
|
+
next unless amount < max_amount
|
100
|
+
|
101
|
+
left_over -= 1
|
102
|
+
subunits_amounts[index] += 1
|
103
|
+
end
|
104
|
+
|
105
|
+
subunits_amounts.map { |cents| Money.from_subunits(cents, allocation_currency) }
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def extract_currency(money_array)
|
111
|
+
currencies = money_array.lazy.select { |money| money.is_a?(Money) }.reject(&:no_currency?).map(&:currency).to_a.uniq
|
112
|
+
if currencies.size > 1
|
113
|
+
raise ArgumentError, "operation not permitted for Money objects with different currencies #{currencies.join(', ')}"
|
114
|
+
end
|
115
|
+
currencies.first || NULL_CURRENCY
|
116
|
+
end
|
117
|
+
|
118
|
+
def amounts_from_splits(allocations, splits, subunits_to_split = subunits)
|
119
|
+
left_over = subunits_to_split
|
120
|
+
|
121
|
+
amounts = splits.collect do |ratio|
|
122
|
+
frac = (Helpers.value_to_decimal(subunits_to_split * ratio) / allocations).floor
|
123
|
+
left_over -= frac
|
124
|
+
frac
|
125
|
+
end
|
126
|
+
|
127
|
+
[amounts, left_over]
|
128
|
+
end
|
129
|
+
|
130
|
+
def all_rational?(splits)
|
131
|
+
splits.all? { |split| split.is_a?(Rational) }
|
132
|
+
end
|
133
|
+
|
134
|
+
def allocation_index_for(strategy, length, idx)
|
135
|
+
case strategy
|
136
|
+
when :roundrobin
|
137
|
+
idx % length
|
138
|
+
when :roundrobin_reverse
|
139
|
+
length - (idx % length) - 1
|
140
|
+
else
|
141
|
+
raise ArgumentError, "Invalid strategy. Valid options: :roundrobin, :roundrobin_reverse"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
data/lib/money/money.rb
CHANGED
@@ -245,94 +245,14 @@ class Money
|
|
245
245
|
Money.new(result, currency)
|
246
246
|
end
|
247
247
|
|
248
|
-
#
|
249
|
-
|
250
|
-
|
251
|
-
# listed first will likely receive more pennies than ones that are listed later
|
252
|
-
#
|
253
|
-
# @param splits [Array<Numeric>]
|
254
|
-
# @return [Array<Money>]
|
255
|
-
#
|
256
|
-
# @example
|
257
|
-
# Money.new(5, "USD").allocate([0.50, 0.25, 0.25])
|
258
|
-
# #=> [#<Money value:2.50 currency:USD>, #<Money value:1.25 currency:USD>, #<Money value:1.25 currency:USD>]
|
259
|
-
# Money.new(5, "USD").allocate([0.3, 0.7])
|
260
|
-
# #=> [#<Money value:1.50 currency:USD>, #<Money value:3.50 currency:USD>]
|
261
|
-
# Money.new(100, "USD").allocate([0.33, 0.33, 0.33])
|
262
|
-
# #=> [#<Money value:33.34 currency:USD>, #<Money value:33.33 currency:USD>, #<Money value:33.33 currency:USD>]
|
263
|
-
|
264
|
-
# @example left over cents distributed to first party due to rounding, and two solutions for a more natural distribution
|
265
|
-
# Money.new(30, "USD").allocate([0.667, 0.333])
|
266
|
-
# #=> [#<Money value:20.01 currency:USD>, #<Money value:9.99 currency:USD>]
|
267
|
-
# Money.new(30, "USD").allocate([0.333, 0.667])
|
268
|
-
# #=> [#<Money value:20.00 currency:USD>, #<Money value:10.00 currency:USD>]
|
269
|
-
# Money.new(30, "USD").allocate([Rational(2, 3), Rational(1, 3)])
|
270
|
-
# #=> [#<Money value:20.00 currency:USD>, #<Money value:10.00 currency:USD>]
|
271
|
-
def allocate(splits)
|
272
|
-
if all_rational?(splits)
|
273
|
-
allocations = splits.inject(0) { |sum, n| sum + n }
|
274
|
-
else
|
275
|
-
allocations = splits.inject(0) { |sum, n| sum + Helpers.value_to_decimal(n) }
|
276
|
-
end
|
277
|
-
|
278
|
-
if (allocations - BigDecimal("1")) > Float::EPSILON
|
279
|
-
raise ArgumentError, "splits add to more than 100%"
|
280
|
-
end
|
281
|
-
|
282
|
-
amounts, left_over = amounts_from_splits(allocations, splits)
|
283
|
-
|
284
|
-
left_over.to_i.times { |i| amounts[i % amounts.length] += 1 }
|
285
|
-
|
286
|
-
amounts.collect { |subunits| Money.from_subunits(subunits, currency) }
|
248
|
+
# @see Money::Allocator#allocate
|
249
|
+
def allocate(splits, strategy = :roundrobin)
|
250
|
+
Money::Allocator.new(self).allocate(splits, strategy)
|
287
251
|
end
|
288
252
|
|
289
|
-
#
|
290
|
-
# Left over pennies will be assigned round-robin up to the maximum specified.
|
291
|
-
# Pennies are dropped when the maximums are attained.
|
292
|
-
#
|
293
|
-
# @example
|
294
|
-
# Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.75)])
|
295
|
-
# #=> [Money.new(26), Money.new(4.75)]
|
296
|
-
#
|
297
|
-
# Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.74)]
|
298
|
-
# #=> [Money.new(26), Money.new(4.74)]
|
299
|
-
#
|
300
|
-
# Money.new(30).allocate_max_amounts([Money.new(15), Money.new(15)]
|
301
|
-
# #=> [Money.new(15), Money.new(15)]
|
302
|
-
#
|
303
|
-
# Money.new(1).allocate_max_amounts([Money.new(33), Money.new(33), Money.new(33)])
|
304
|
-
# #=> [Money.new(0.34), Money.new(0.33), Money.new(0.33)]
|
305
|
-
#
|
306
|
-
# Money.new(100).allocate_max_amounts([Money.new(5), Money.new(2)])
|
307
|
-
# #=> [Money.new(5), Money.new(2)]
|
253
|
+
# @see Money::Allocator#allocate_max_amounts
|
308
254
|
def allocate_max_amounts(maximums)
|
309
|
-
|
310
|
-
maximums = maximums.map { |max| max.to_money(allocation_currency) }
|
311
|
-
maximums_total = maximums.reduce(Money.new(0, allocation_currency), :+)
|
312
|
-
|
313
|
-
splits = maximums.map do |max_amount|
|
314
|
-
next(0) if maximums_total.zero?
|
315
|
-
Money.rational(max_amount, maximums_total)
|
316
|
-
end
|
317
|
-
|
318
|
-
total_allocatable = [
|
319
|
-
value * allocation_currency.subunit_to_unit,
|
320
|
-
maximums_total.value * allocation_currency.subunit_to_unit
|
321
|
-
].min
|
322
|
-
|
323
|
-
subunits_amounts, left_over = amounts_from_splits(1, splits, total_allocatable)
|
324
|
-
|
325
|
-
subunits_amounts.each_with_index do |amount, index|
|
326
|
-
break unless left_over > 0
|
327
|
-
|
328
|
-
max_amount = maximums[index].value * allocation_currency.subunit_to_unit
|
329
|
-
next unless amount < max_amount
|
330
|
-
|
331
|
-
left_over -= 1
|
332
|
-
subunits_amounts[index] += 1
|
333
|
-
end
|
334
|
-
|
335
|
-
subunits_amounts.map { |cents| Money.from_subunits(cents, allocation_currency) }
|
255
|
+
Money::Allocator.new(self).allocate_max_amounts(maximums)
|
336
256
|
end
|
337
257
|
|
338
258
|
# Split money amongst parties evenly without losing pennies.
|
@@ -395,22 +315,6 @@ class Money
|
|
395
315
|
|
396
316
|
private
|
397
317
|
|
398
|
-
def all_rational?(splits)
|
399
|
-
splits.all? { |split| split.is_a?(Rational) }
|
400
|
-
end
|
401
|
-
|
402
|
-
def amounts_from_splits(allocations, splits, subunits_to_split = subunits)
|
403
|
-
left_over = subunits_to_split
|
404
|
-
|
405
|
-
amounts = splits.collect do |ratio|
|
406
|
-
frac = (Helpers.value_to_decimal(subunits_to_split * ratio) / allocations).floor
|
407
|
-
left_over -= frac
|
408
|
-
frac
|
409
|
-
end
|
410
|
-
|
411
|
-
[amounts, left_over]
|
412
|
-
end
|
413
|
-
|
414
318
|
def arithmetic(money_or_numeric)
|
415
319
|
raise TypeError, "#{money_or_numeric.class.name} can't be coerced into Money" unless money_or_numeric.respond_to?(:to_money)
|
416
320
|
other = money_or_numeric.to_money(currency)
|
@@ -421,12 +325,4 @@ class Money
|
|
421
325
|
def calculated_currency(other)
|
422
326
|
no_currency? ? other : currency
|
423
327
|
end
|
424
|
-
|
425
|
-
def extract_currency(money_array)
|
426
|
-
currencies = money_array.lazy.select { |money| money.is_a?(Money) }.reject(&:no_currency?).map(&:currency).to_a.uniq
|
427
|
-
if currencies.size > 1
|
428
|
-
raise ArgumentError, "operation not permitted for Money objects with different currencies #{currencies.join(', ')}"
|
429
|
-
end
|
430
|
-
currencies.first || NULL_CURRENCY
|
431
|
-
end
|
432
328
|
end
|
data/lib/money/version.rb
CHANGED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe "Allocator" do
|
4
|
+
describe "allocate"do
|
5
|
+
specify "#allocate takes no action when one gets all" do
|
6
|
+
expect(new_allocator(5).allocate([1])).to eq([Money.new(5)])
|
7
|
+
end
|
8
|
+
|
9
|
+
specify "#allocate does not lose pennies" do
|
10
|
+
moneys = new_allocator(0.05).allocate([0.3,0.7])
|
11
|
+
expect(moneys[0]).to eq(Money.new(0.02))
|
12
|
+
expect(moneys[1]).to eq(Money.new(0.03))
|
13
|
+
end
|
14
|
+
|
15
|
+
specify "#allocate does not lose dollars with non-decimal currency" do
|
16
|
+
moneys = new_allocator(5, 'JPY').allocate([0.3,0.7])
|
17
|
+
expect(moneys[0]).to eq(Money.new(2, 'JPY'))
|
18
|
+
expect(moneys[1]).to eq(Money.new(3, 'JPY'))
|
19
|
+
end
|
20
|
+
|
21
|
+
specify "#allocate does not lose dollars with three decimal currency" do
|
22
|
+
moneys = new_allocator(0.005, 'JOD').allocate([0.3,0.7])
|
23
|
+
expect(moneys[0]).to eq(Money.new(0.002, 'JOD'))
|
24
|
+
expect(moneys[1]).to eq(Money.new(0.003, 'JOD'))
|
25
|
+
end
|
26
|
+
|
27
|
+
specify "#allocate does not lose pennies even when given a lossy split" do
|
28
|
+
moneys = new_allocator(1).allocate([0.333,0.333, 0.333])
|
29
|
+
expect(moneys[0].subunits).to eq(34)
|
30
|
+
expect(moneys[1].subunits).to eq(33)
|
31
|
+
expect(moneys[2].subunits).to eq(33)
|
32
|
+
end
|
33
|
+
|
34
|
+
specify "#allocate requires total to be less than 1" do
|
35
|
+
expect { new_allocator(0.05).allocate([0.5,0.6]) }.to raise_error(ArgumentError)
|
36
|
+
end
|
37
|
+
|
38
|
+
specify "#allocate will use rationals if provided" do
|
39
|
+
splits = [128400,20439,14589,14589,25936].map{ |num| Rational(num, 203953) } # sums to > 1 if converted to float
|
40
|
+
expect(new_allocator(2.25).allocate(splits)).to eq([Money.new(1.42), Money.new(0.23), Money.new(0.16), Money.new(0.16), Money.new(0.28)])
|
41
|
+
end
|
42
|
+
|
43
|
+
specify "#allocate will convert rationals with high precision" do
|
44
|
+
ratios = [Rational(1, 1), Rational(0)]
|
45
|
+
expect(new_allocator("858993456.12").allocate(ratios)).to eq([Money.new("858993456.12"), Money.empty])
|
46
|
+
ratios = [Rational(1, 6), Rational(5, 6)]
|
47
|
+
expect(new_allocator("3.00").allocate(ratios)).to eq([Money.new("0.50"), Money.new("2.50")])
|
48
|
+
end
|
49
|
+
|
50
|
+
specify "#allocate doesn't raise with weird negative rational ratios" do
|
51
|
+
rate = Rational(-5, 1201)
|
52
|
+
expect { new_allocator(1).allocate([rate, 1 - rate]) }.not_to raise_error
|
53
|
+
end
|
54
|
+
|
55
|
+
specify "#allocate fills pennies from beginning to end with roundrobin strategy" do
|
56
|
+
moneys = new_allocator(0.05).allocate([0.3,0.7], :roundrobin)
|
57
|
+
expect(moneys[0]).to eq(Money.new(0.02))
|
58
|
+
expect(moneys[1]).to eq(Money.new(0.03))
|
59
|
+
end
|
60
|
+
|
61
|
+
specify "#allocate fills pennies from end to beginning with roundrobin_reverse strategy" do
|
62
|
+
moneys = new_allocator(0.05).allocate([0.3,0.7], :roundrobin_reverse)
|
63
|
+
expect(moneys[0]).to eq(Money.new(0.01))
|
64
|
+
expect(moneys[1]).to eq(Money.new(0.04))
|
65
|
+
end
|
66
|
+
|
67
|
+
specify "#allocate raise ArgumentError when invalid strategy is provided" do
|
68
|
+
expect { new_allocator(0.03).allocate([0.5, 0.5], :bad_strategy_name) }.to raise_error(ArgumentError, "Invalid strategy. Valid options: :roundrobin, :roundrobin_reverse")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'allocate_max_amounts' do
|
73
|
+
specify "#allocate_max_amounts returns the weighted allocation without exceeding the maxima when there is room for the remainder" do
|
74
|
+
expect(
|
75
|
+
new_allocator(30.75).allocate_max_amounts([Money.new(26), Money.new(4.75)]),
|
76
|
+
).to eq([Money.new(26), Money.new(4.75)])
|
77
|
+
end
|
78
|
+
|
79
|
+
specify "#allocate_max_amounts returns the weighted allocation without exceeding the maxima when there is room for the remainder with currency" do
|
80
|
+
expect(
|
81
|
+
new_allocator(3075, 'JPY').allocate_max_amounts([Money.new(2600, 'JPY'), Money.new(475, 'JPY')]),
|
82
|
+
).to eq([Money.new(2600, 'JPY'), Money.new(475, 'JPY')])
|
83
|
+
end
|
84
|
+
|
85
|
+
specify "#allocate_max_amounts legal computation with no currency objects" do
|
86
|
+
expect(
|
87
|
+
new_allocator(3075, 'JPY').allocate_max_amounts([2600, 475]),
|
88
|
+
).to eq([Money.new(2600, 'JPY'), Money.new(475, 'JPY')])
|
89
|
+
|
90
|
+
expect(
|
91
|
+
new_allocator(3075, Money::NULL_CURRENCY).allocate_max_amounts([Money.new(2600, 'JPY'), Money.new(475, 'JPY')]),
|
92
|
+
).to eq([Money.new(2600, 'JPY'), Money.new(475, 'JPY')])
|
93
|
+
end
|
94
|
+
|
95
|
+
specify "#allocate_max_amounts illegal computation across currencies" do
|
96
|
+
expect {
|
97
|
+
new_allocator(3075, 'USD').allocate_max_amounts([Money.new(2600, 'JPY'), Money.new(475, 'JPY')])
|
98
|
+
}.to raise_error(ArgumentError)
|
99
|
+
end
|
100
|
+
|
101
|
+
specify "#allocate_max_amounts drops the remainder when returning the weighted allocation without exceeding the maxima when there is no room for the remainder" do
|
102
|
+
expect(
|
103
|
+
new_allocator(30.75).allocate_max_amounts([Money.new(26), Money.new(4.74)]),
|
104
|
+
).to eq([Money.new(26), Money.new(4.74)])
|
105
|
+
end
|
106
|
+
|
107
|
+
specify "#allocate_max_amounts returns the weighted allocation when there is no remainder" do
|
108
|
+
expect(
|
109
|
+
new_allocator(30).allocate_max_amounts([Money.new(15), Money.new(15)]),
|
110
|
+
).to eq([Money.new(15), Money.new(15)])
|
111
|
+
end
|
112
|
+
|
113
|
+
specify "#allocate_max_amounts allocates the remainder round-robin when the maxima are not reached" do
|
114
|
+
expect(
|
115
|
+
new_allocator(1).allocate_max_amounts([Money.new(33), Money.new(33), Money.new(33)]),
|
116
|
+
).to eq([Money.new(0.34), Money.new(0.33), Money.new(0.33)])
|
117
|
+
end
|
118
|
+
|
119
|
+
specify "#allocate_max_amounts allocates up to the maxima specified" do
|
120
|
+
expect(
|
121
|
+
new_allocator(100).allocate_max_amounts([Money.new(5), Money.new(2)]),
|
122
|
+
).to eq([Money.new(5), Money.new(2)])
|
123
|
+
end
|
124
|
+
|
125
|
+
specify "#allocate_max_amounts supports all-zero maxima" do
|
126
|
+
expect(
|
127
|
+
new_allocator(3).allocate_max_amounts([Money.empty, Money.empty, Money.empty]),
|
128
|
+
).to eq([Money.empty, Money.empty, Money.empty])
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def new_allocator(amount, currency = nil)
|
133
|
+
Money::Allocator.new(Money.new(amount, currency))
|
134
|
+
end
|
135
|
+
end
|
data/spec/money_spec.rb
CHANGED
@@ -583,113 +583,29 @@ RSpec.describe "Money" do
|
|
583
583
|
end
|
584
584
|
|
585
585
|
describe "allocation"do
|
586
|
-
specify "#allocate
|
587
|
-
|
586
|
+
specify "#allocate is calculated by Money::Allocator#allocate" do
|
587
|
+
expected = [Money.new(1), [Money.new(1)]]
|
588
|
+
expect_any_instance_of(Money::Allocator).to receive(:allocate).with([0.5, 0.5], :roundrobin).and_return(expected)
|
589
|
+
expect(Money.new(2).allocate([0.5, 0.5])).to eq(expected)
|
588
590
|
end
|
589
591
|
|
590
|
-
specify "#allocate does not lose pennies" do
|
592
|
+
specify "#allocate does not lose pennies (integration test)" do
|
591
593
|
moneys = Money.new(0.05).allocate([0.3,0.7])
|
592
594
|
expect(moneys[0]).to eq(Money.new(0.02))
|
593
595
|
expect(moneys[1]).to eq(Money.new(0.03))
|
594
596
|
end
|
595
597
|
|
596
|
-
specify "#
|
597
|
-
|
598
|
-
|
599
|
-
expect(
|
598
|
+
specify "#allocate_max_amounts is calculated by Money::Allocator#allocate_max_amounts" do
|
599
|
+
expected = [Money.new(1), [Money.new(1)]]
|
600
|
+
expect_any_instance_of(Money::Allocator).to receive(:allocate_max_amounts).and_return(expected)
|
601
|
+
expect(Money.new(2).allocate_max_amounts([0.5, 0.5])).to eq(expected)
|
600
602
|
end
|
601
603
|
|
602
|
-
specify "#
|
603
|
-
moneys = Money.new(0.005, 'JOD').allocate([0.3,0.7])
|
604
|
-
expect(moneys[0]).to eq(Money.new(0.002, 'JOD'))
|
605
|
-
expect(moneys[1]).to eq(Money.new(0.003, 'JOD'))
|
606
|
-
end
|
607
|
-
|
608
|
-
specify "#allocate does not lose pennies even when given a lossy split" do
|
609
|
-
moneys = Money.new(1).allocate([0.333,0.333, 0.333])
|
610
|
-
expect(moneys[0].subunits).to eq(34)
|
611
|
-
expect(moneys[1].subunits).to eq(33)
|
612
|
-
expect(moneys[2].subunits).to eq(33)
|
613
|
-
end
|
614
|
-
|
615
|
-
specify "#allocate requires total to be less than 1" do
|
616
|
-
expect { Money.new(0.05).allocate([0.5,0.6]) }.to raise_error(ArgumentError)
|
617
|
-
end
|
618
|
-
|
619
|
-
specify "#allocate will use rationals if provided" do
|
620
|
-
splits = [128400,20439,14589,14589,25936].map{ |num| Rational(num, 203953) } # sums to > 1 if converted to float
|
621
|
-
expect(Money.new(2.25).allocate(splits)).to eq([Money.new(1.42), Money.new(0.23), Money.new(0.16), Money.new(0.16), Money.new(0.28)])
|
622
|
-
end
|
623
|
-
|
624
|
-
specify "#allocate will convert rationals with high precision" do
|
625
|
-
ratios = [Rational(1, 1), Rational(0)]
|
626
|
-
expect(Money.new("858993456.12").allocate(ratios)).to eq([Money.new("858993456.12"), Money.empty])
|
627
|
-
ratios = [Rational(1, 6), Rational(5, 6)]
|
628
|
-
expect(Money.new("3.00").allocate(ratios)).to eq([Money.new("0.50"), Money.new("2.50")])
|
629
|
-
end
|
630
|
-
|
631
|
-
specify "#allocate doesn't raise with weird negative rational ratios" do
|
632
|
-
rate = Rational(-5, 1201)
|
633
|
-
expect { Money.new(1).allocate([rate, 1 - rate]) }.not_to raise_error
|
634
|
-
end
|
635
|
-
|
636
|
-
specify "#allocate_max_amounts returns the weighted allocation without exceeding the maxima when there is room for the remainder" do
|
604
|
+
specify "#allocate_max_amounts returns the weighted allocation without exceeding the maxima when there is room for the remainder (integration test)" do
|
637
605
|
expect(
|
638
606
|
Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.75)]),
|
639
607
|
).to eq([Money.new(26), Money.new(4.75)])
|
640
608
|
end
|
641
|
-
|
642
|
-
specify "#allocate_max_amounts returns the weighted allocation without exceeding the maxima when there is room for the remainder with currency" do
|
643
|
-
expect(
|
644
|
-
Money.new(3075, 'JPY').allocate_max_amounts([Money.new(2600, 'JPY'), Money.new(475, 'JPY')]),
|
645
|
-
).to eq([Money.new(2600, 'JPY'), Money.new(475, 'JPY')])
|
646
|
-
end
|
647
|
-
|
648
|
-
specify "#allocate_max_amounts legal computation with no currency objects" do
|
649
|
-
expect(
|
650
|
-
Money.new(3075, 'JPY').allocate_max_amounts([2600, 475]),
|
651
|
-
).to eq([Money.new(2600, 'JPY'), Money.new(475, 'JPY')])
|
652
|
-
|
653
|
-
expect(
|
654
|
-
Money.new(3075, Money::NULL_CURRENCY).allocate_max_amounts([Money.new(2600, 'JPY'), Money.new(475, 'JPY')]),
|
655
|
-
).to eq([Money.new(2600, 'JPY'), Money.new(475, 'JPY')])
|
656
|
-
end
|
657
|
-
|
658
|
-
specify "#allocate_max_amounts illegal computation across currencies" do
|
659
|
-
expect {
|
660
|
-
Money.new(3075, 'USD').allocate_max_amounts([Money.new(2600, 'JPY'), Money.new(475, 'JPY')])
|
661
|
-
}.to raise_error(ArgumentError)
|
662
|
-
end
|
663
|
-
|
664
|
-
specify "#allocate_max_amounts drops the remainder when returning the weighted allocation without exceeding the maxima when there is no room for the remainder" do
|
665
|
-
expect(
|
666
|
-
Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.74)]),
|
667
|
-
).to eq([Money.new(26), Money.new(4.74)])
|
668
|
-
end
|
669
|
-
|
670
|
-
specify "#allocate_max_amounts returns the weighted allocation when there is no remainder" do
|
671
|
-
expect(
|
672
|
-
Money.new(30).allocate_max_amounts([Money.new(15), Money.new(15)]),
|
673
|
-
).to eq([Money.new(15), Money.new(15)])
|
674
|
-
end
|
675
|
-
|
676
|
-
specify "#allocate_max_amounts allocates the remainder round-robin when the maxima are not reached" do
|
677
|
-
expect(
|
678
|
-
Money.new(1).allocate_max_amounts([Money.new(33), Money.new(33), Money.new(33)]),
|
679
|
-
).to eq([Money.new(0.34), Money.new(0.33), Money.new(0.33)])
|
680
|
-
end
|
681
|
-
|
682
|
-
specify "#allocate_max_amounts allocates up to the maxima specified" do
|
683
|
-
expect(
|
684
|
-
Money.new(100).allocate_max_amounts([Money.new(5), Money.new(2)]),
|
685
|
-
).to eq([Money.new(5), Money.new(2)])
|
686
|
-
end
|
687
|
-
|
688
|
-
specify "#allocate_max_amounts supports all-zero maxima" do
|
689
|
-
expect(
|
690
|
-
Money.new(3).allocate_max_amounts([Money.empty, Money.empty, Money.empty]),
|
691
|
-
).to eq([Money.empty, Money.empty, Money.empty])
|
692
|
-
end
|
693
609
|
end
|
694
610
|
|
695
611
|
describe "split" do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shopify-money
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-06-
|
11
|
+
date: 2019-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -131,6 +131,7 @@ files:
|
|
131
131
|
- dev.yml
|
132
132
|
- lib/money.rb
|
133
133
|
- lib/money/accounting_money_parser.rb
|
134
|
+
- lib/money/allocator.rb
|
134
135
|
- lib/money/core_extensions.rb
|
135
136
|
- lib/money/currency.rb
|
136
137
|
- lib/money/currency/loader.rb
|
@@ -148,6 +149,7 @@ files:
|
|
148
149
|
- lib/money_column/railtie.rb
|
149
150
|
- money.gemspec
|
150
151
|
- spec/accounting_money_parser_spec.rb
|
152
|
+
- spec/allocator_spec.rb
|
151
153
|
- spec/core_extensions_spec.rb
|
152
154
|
- spec/currency/loader_spec.rb
|
153
155
|
- spec/currency_spec.rb
|
@@ -185,6 +187,7 @@ specification_version: 4
|
|
185
187
|
summary: Shopify's money gem
|
186
188
|
test_files:
|
187
189
|
- spec/accounting_money_parser_spec.rb
|
190
|
+
- spec/allocator_spec.rb
|
188
191
|
- spec/core_extensions_spec.rb
|
189
192
|
- spec/currency/loader_spec.rb
|
190
193
|
- spec/currency_spec.rb
|