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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e95441c6aa9e01d7ac17c8e76f8008cf94dba8d4af2bd75f101d743a333311e
4
- data.tar.gz: 07d99a0656016292183ef0a782879f018d4a8fdb814ab6e97470675003280805
3
+ metadata.gz: ca42130a743b47dba4be201df379a894d92e9e6101338fd41c6ad84b94805997
4
+ data.tar.gz: 4b4749500c63bdb88d8f63f36628a845f355aad3f572e428dd60804ad107b591
5
5
  SHA512:
6
- metadata.gz: e18f193d30427adb99f4ebb28e1125a5819ec489801c25698069f1c48448edb492ea3effbd9117bac45d77a613f59688a5ff5ed3327d4b20d03b6dd0a51231d7
7
- data.tar.gz: b5cc2766b02db02ebf0df626cc531d0996444f78e7939c7fee7e3caa050cb2fb06c767e88e7256ad12131f225436c68b7ebf776c6e8ea1995e18525004f34896
6
+ metadata.gz: 9bffc191b4fea246018814cd3961b5bd298212d076c6f6eb6b82d8c19bd377387f360b1a982dc1978ae8a29f531dd57e7127d47f5f4192b0fe5246a8015bc7f2
7
+ data.tar.gz: d99c2ca80783732eda61cb4e064140131b0aee8da9f56b541e9e5fad7cd1fa4c9ec7362c9fb343ae0c2716888135a38f76799435727f91f025ccddeede94aba4
@@ -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
@@ -245,94 +245,14 @@ class Money
245
245
  Money.new(result, currency)
246
246
  end
247
247
 
248
- # Allocates money between different parties without losing pennies.
249
- # After the mathematically split has been performed, left over pennies will
250
- # be distributed round-robin amongst the parties. This means that parties
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
- # Allocates money between different parties up to the maximum amounts specified.
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
- allocation_currency = extract_currency(maximums + [self])
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
@@ -1,3 +1,3 @@
1
1
  class Money
2
- VERSION = "0.12.0"
2
+ VERSION = "0.13.0"
3
3
  end
@@ -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
@@ -583,113 +583,29 @@ RSpec.describe "Money" do
583
583
  end
584
584
 
585
585
  describe "allocation"do
586
- specify "#allocate takes no action when one gets all" do
587
- expect(Money.new(5).allocate([1])).to eq([Money.new(5)])
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 "#allocate does not lose dollars with non-decimal currency" do
597
- moneys = Money.new(5, 'JPY').allocate([0.3,0.7])
598
- expect(moneys[0]).to eq(Money.new(2, 'JPY'))
599
- expect(moneys[1]).to eq(Money.new(3, 'JPY'))
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 "#allocate does not lose dollars with three decimal currency" do
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.12.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-10 00:00:00.000000000 Z
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