shopify-money 0.12.0 → 0.13.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.
- 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
|