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