shopify-money 2.0.0 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/money/money.rb CHANGED
@@ -10,6 +10,30 @@ class Money
10
10
  attr_reader :value, :currency
11
11
  def_delegators :@value, :zero?, :nonzero?, :positive?, :negative?, :to_i, :to_f, :hash
12
12
 
13
+ class ReverseOperationProxy
14
+ include Comparable
15
+
16
+ def initialize(value)
17
+ @value = value
18
+ end
19
+
20
+ def <=>(other)
21
+ -(other <=> @value)
22
+ end
23
+
24
+ def +(other)
25
+ other + @value
26
+ end
27
+
28
+ def -(other)
29
+ -(other - @value)
30
+ end
31
+
32
+ def *(other)
33
+ other * @value
34
+ end
35
+ end
36
+
13
37
  class << self
14
38
  extend Forwardable
15
39
  attr_accessor :config
@@ -21,6 +45,8 @@ class Money
21
45
  end
22
46
 
23
47
  def new(value = 0, currency = nil)
48
+ return new_from_money(value, currency) if value.is_a?(Money)
49
+
24
50
  value = Helpers.value_to_decimal(value)
25
51
  currency = Helpers.value_to_currency(currency)
26
52
 
@@ -76,6 +102,28 @@ class Money
76
102
  Money.current_currency = old_currency
77
103
  end
78
104
  end
105
+
106
+ private
107
+
108
+ def new_from_money(amount, currency)
109
+ currency = Helpers.value_to_currency(currency)
110
+
111
+ if amount.no_currency?
112
+ return Money.new(amount.value, currency)
113
+ end
114
+
115
+ if amount.currency.compatible?(currency)
116
+ return amount
117
+ end
118
+
119
+ msg = "Money.new is attempting to change currency of an existing money object"
120
+ if Money.config.legacy_deprecations
121
+ Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
122
+ return Money.new(amount.value, currency)
123
+ else
124
+ raise Money::IncompatibleCurrencyError, msg
125
+ end
126
+ end
79
127
  end
80
128
  configure
81
129
 
@@ -162,50 +210,23 @@ class Money
162
210
  value == other.value
163
211
  end
164
212
 
165
- class ReverseOperationProxy
166
- include Comparable
167
-
168
- def initialize(value)
169
- @value = value
170
- end
171
-
172
- def <=>(other)
173
- -(other <=> @value)
174
- end
175
-
176
- def +(other)
177
- other + @value
178
- end
179
-
180
- def -(other)
181
- -(other - @value)
182
- end
183
-
184
- def *(other)
185
- other * @value
186
- end
187
- end
188
-
189
213
  def coerce(other)
190
214
  raise TypeError, "Money can't be coerced into #{other.class}" unless other.is_a?(Numeric)
191
215
  [ReverseOperationProxy.new(other), self]
192
216
  end
193
217
 
194
- def to_money(curr = nil)
195
- if !curr.nil? && no_currency?
196
- return Money.new(value, curr)
218
+ def to_money(new_currency = nil)
219
+ if new_currency.nil?
220
+ return self
197
221
  end
198
222
 
199
- curr = Helpers.value_to_currency(curr)
200
- unless currency.compatible?(curr)
201
- msg = "mathematical operation not permitted for Money objects with different currencies #{curr} and #{currency}"
202
- if Money.config.legacy_deprecations
203
- Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
204
- else
205
- raise Money::IncompatibleCurrencyError, msg
206
- end
223
+ if no_currency?
224
+ return Money.new(value, new_currency)
207
225
  end
208
226
 
227
+ ensure_compatible_currency(Helpers.value_to_currency(new_currency),
228
+ "to_money is attempting to change currency of an existing money object from #{currency} to #{new_currency}")
229
+
209
230
  self
210
231
  end
211
232
 
@@ -293,12 +314,12 @@ class Money
293
314
  #
294
315
  # @param [2] number of parties.
295
316
  #
296
- # @return [Array<Money, Money, Money>]
317
+ # @return [Enumerable<Money, Money, Money>]
297
318
  #
298
319
  # @example
299
- # Money.new(100, "USD").split(3) #=> [Money.new(34), Money.new(33), Money.new(33)]
320
+ # Money.new(100, "USD").split(3) #=> Enumerable[Money.new(34), Money.new(33), Money.new(33)]
300
321
  def split(num)
301
- calculate_splits(num).sum([]) { |value, count| Array.new(count, value) }
322
+ Splitter.new(self, num)
302
323
  end
303
324
 
304
325
  # Calculate the splits evenly without losing pennies.
@@ -313,17 +334,7 @@ class Money
313
334
  # @example
314
335
  # Money.new(100, "USD").calculate_splits(3) #=> {Money.new(34) => 1, Money.new(33) => 2}
315
336
  def calculate_splits(num)
316
- raise ArgumentError, "need at least one party" if num < 1
317
- subunits = self.subunits
318
- low = Money.from_subunits(subunits / num, currency)
319
- high = Money.from_subunits(low.subunits + 1, currency)
320
-
321
- num_high = subunits % num
322
-
323
- {}.tap do |result|
324
- result[high] = num_high if num_high > 0
325
- result[low] = num - num_high
326
- end
337
+ Splitter.new(self, num).split.dup
327
338
  end
328
339
 
329
340
  # Clamps the value to be within the specified minimum and maximum. Returns
@@ -349,11 +360,34 @@ class Money
349
360
 
350
361
  private
351
362
 
352
- def arithmetic(money_or_numeric)
353
- raise TypeError, "#{money_or_numeric.class.name} can't be coerced into Money" unless money_or_numeric.respond_to?(:to_money)
354
- other = money_or_numeric.to_money(currency)
363
+ def arithmetic(other)
364
+ case other
365
+ when Money
366
+ ensure_compatible_currency(other.currency,
367
+ "mathematical operation not permitted for Money objects with different currencies #{other.currency} and #{currency}.")
368
+ yield(other)
355
369
 
356
- yield(other)
370
+ when Numeric, String
371
+ yield(Money.new(other, currency))
372
+
373
+ else
374
+ if Money.config.legacy_deprecations && other.respond_to?(:to_money)
375
+ Money.deprecate("#{other.inspect} is being implicitly coerced into a Money object. Call `to_money` on this object to transform it into a money explicitly. An TypeError will raise in the next major release")
376
+ yield(other.to_money(currency))
377
+ else
378
+ raise TypeError, "#{other.class.name} can't be coerced into Money"
379
+ end
380
+ end
381
+ end
382
+
383
+ def ensure_compatible_currency(other_currency, msg)
384
+ return if currency.compatible?(other_currency)
385
+
386
+ if Money.config.legacy_deprecations
387
+ Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
388
+ else
389
+ raise Money::IncompatibleCurrencyError, msg
390
+ end
357
391
  end
358
392
 
359
393
  def calculated_currency(other)
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Money
4
+ class Splitter
5
+ include Enumerable
6
+
7
+ def initialize(money, num)
8
+ @num = Integer(num)
9
+ raise ArgumentError, "need at least one party" if num < 1
10
+ @money = money
11
+ @split = nil
12
+ end
13
+
14
+ protected attr_writer :split
15
+
16
+ def split
17
+ @split ||= begin
18
+ subunits = @money.subunits
19
+ low = Money.from_subunits(subunits / @num, @money.currency)
20
+ high = Money.from_subunits(low.subunits + 1, @money.currency)
21
+
22
+ num_high = subunits % @num
23
+
24
+ split = {}
25
+ split[high] = num_high if num_high > 0
26
+ split[low] = @num - num_high
27
+ split.freeze
28
+ end
29
+ end
30
+
31
+ alias_method :to_ary, :to_a
32
+
33
+ def first(count = (count_undefined = true))
34
+ if count_undefined
35
+ each do |money|
36
+ return money
37
+ end
38
+ else
39
+ if count >= size
40
+ to_a
41
+ else
42
+ result = Array.new(count)
43
+ index = 0
44
+ each do |money|
45
+ result[index] = money
46
+ index += 1
47
+ break if index == count
48
+ end
49
+ result
50
+ end
51
+ end
52
+ end
53
+
54
+ def last(count = (count_undefined = true))
55
+ if count_undefined
56
+ reverse_each do |money|
57
+ return money
58
+ end
59
+ else
60
+ if count >= size
61
+ to_a
62
+ else
63
+ result = Array.new(count)
64
+ index = 0
65
+ reverse_each do |money|
66
+ result[index] = money
67
+ index += 1
68
+ break if index == count
69
+ end
70
+ result.reverse!
71
+ result
72
+ end
73
+ end
74
+ end
75
+
76
+ def [](index)
77
+ offset = 0
78
+ split.each do |money, count|
79
+ offset += count
80
+ if index < offset
81
+ return money
82
+ end
83
+ end
84
+ nil
85
+ end
86
+
87
+ def reverse_each(&block)
88
+ split.reverse_each do |money, count|
89
+ count.times do
90
+ yield money
91
+ end
92
+ end
93
+ end
94
+
95
+ def each(&block)
96
+ split.each do |money, count|
97
+ count.times do
98
+ yield money
99
+ end
100
+ end
101
+ end
102
+
103
+ def reverse
104
+ copy = dup
105
+ copy.split = split.reverse_each.to_h.freeze
106
+ copy
107
+ end
108
+
109
+ def size
110
+ count = 0
111
+ split.each_value { |c| count += c }
112
+ count
113
+ end
114
+ end
115
+ end
data/lib/money/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  class Money
3
- VERSION = "2.0.0"
3
+ VERSION = "2.2.1"
4
4
  end
data/lib/money.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'money/helpers'
5
5
  require_relative 'money/currency'
6
6
  require_relative 'money/null_currency'
7
7
  require_relative 'money/allocator'
8
+ require_relative 'money/splitter'
8
9
  require_relative 'money/config'
9
10
  require_relative 'money/money'
10
11
  require_relative 'money/errors'
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rubocop/cop/money/missing_currency'
4
- require 'rubocop/cop/money/unsafe_to_money'
5
4
  require 'rubocop/cop/money/zero_money'
data/money.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
20
20
  s.add_development_dependency("rails", "~> 6.0")
21
21
  s.add_development_dependency("rspec", "~> 3.2")
22
22
  s.add_development_dependency("database_cleaner", "~> 1.6")
23
- s.add_development_dependency("sqlite3", "~> 1.4.2")
23
+ s.add_development_dependency("sqlite3")
24
24
 
25
25
  s.required_ruby_version = '>= 3.0'
26
26
 
@@ -8,34 +8,38 @@ RSpec.describe "Allocator" do
8
8
  end
9
9
 
10
10
  specify "#allocate does not lose pennies" do
11
- moneys = new_allocator(0.05).allocate([0.3,0.7])
12
- expect(moneys[0]).to eq(Money.new(0.02))
13
- expect(moneys[1]).to eq(Money.new(0.03))
11
+ monies = new_allocator(0.05).allocate([0.3,0.7])
12
+ expect(monies[0]).to eq(Money.new(0.02))
13
+ expect(monies[1]).to eq(Money.new(0.03))
14
14
  end
15
15
 
16
16
  specify "#allocate does not lose dollars with non-decimal currency" do
17
- moneys = new_allocator(5, 'JPY').allocate([0.3,0.7])
18
- expect(moneys[0]).to eq(Money.new(2, 'JPY'))
19
- expect(moneys[1]).to eq(Money.new(3, 'JPY'))
17
+ monies = new_allocator(5, 'JPY').allocate([0.3,0.7])
18
+ expect(monies[0]).to eq(Money.new(2, 'JPY'))
19
+ expect(monies[1]).to eq(Money.new(3, 'JPY'))
20
20
  end
21
21
 
22
22
  specify "#allocate does not lose dollars with three decimal currency" do
23
- moneys = new_allocator(0.005, 'JOD').allocate([0.3,0.7])
24
- expect(moneys[0]).to eq(Money.new(0.002, 'JOD'))
25
- expect(moneys[1]).to eq(Money.new(0.003, 'JOD'))
23
+ monies = new_allocator(0.005, 'JOD').allocate([0.3,0.7])
24
+ expect(monies[0]).to eq(Money.new(0.002, 'JOD'))
25
+ expect(monies[1]).to eq(Money.new(0.003, 'JOD'))
26
26
  end
27
27
 
28
28
  specify "#allocate does not lose pennies even when given a lossy split" do
29
- moneys = new_allocator(1).allocate([0.333,0.333, 0.333])
30
- expect(moneys[0].subunits).to eq(34)
31
- expect(moneys[1].subunits).to eq(33)
32
- expect(moneys[2].subunits).to eq(33)
29
+ monies = new_allocator(1).allocate([0.333,0.333, 0.333])
30
+ expect(monies[0].subunits).to eq(34)
31
+ expect(monies[1].subunits).to eq(33)
32
+ expect(monies[2].subunits).to eq(33)
33
33
  end
34
34
 
35
35
  specify "#allocate requires total to be less than 1" do
36
36
  expect { new_allocator(0.05).allocate([0.5,0.6]) }.to raise_error(ArgumentError)
37
37
  end
38
38
 
39
+ specify "#allocate requires at least one split" do
40
+ expect { new_allocator(0.05).allocate([]) }.to raise_error(ArgumentError)
41
+ end
42
+
39
43
  specify "#allocate will use rationals if provided" do
40
44
  splits = [128400,20439,14589,14589,25936].map{ |num| Rational(num, 203953) } # sums to > 1 if converted to float
41
45
  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)])
@@ -53,27 +57,173 @@ RSpec.describe "Allocator" do
53
57
  expect { new_allocator(1).allocate([rate, 1 - rate]) }.not_to raise_error
54
58
  end
55
59
 
60
+ specify "#allocate raise ArgumentError when invalid strategy is provided" do
61
+ expect { new_allocator(0.03).allocate([0.5, 0.5], :bad_strategy_name) }.to raise_error(ArgumentError, "Invalid strategy. Valid options: :roundrobin, :roundrobin_reverse, :nearest")
62
+ end
63
+
64
+ specify "#allocate raises an error when splits exceed 100%" do
65
+ expect { new_allocator(0.03).allocate([0.5, 0.6]) }.to raise_error(ArgumentError, "allocations add to more than 100%")
66
+ end
67
+
68
+ specify "#allocate scales up allocations less than 100%, preserving the relative magnitude of each chunk" do
69
+ # Allocations sum to 0.3
70
+ # This is analogous to new_allocator(12).allocate([1/3, 2/3])
71
+ monies = new_allocator(12).allocate([0.1, 0.2])
72
+ expect(monies[0]).to eq(Money.new(4))
73
+ expect(monies[1]).to eq(Money.new(8))
74
+
75
+ # Allocations sum to .661
76
+ monies = new_allocator(488.51).allocate([0.111, 0.05, 0.5])
77
+ expect(monies[0]).to eq(Money.new(82.04)) # <-- leftover penny
78
+ expect(monies[1]).to eq(Money.new(36.95))
79
+ expect(monies[2]).to eq(Money.new(369.52))
80
+
81
+ monies = new_allocator(488.51).allocate([0.111, 0.05, 0.5], :roundrobin_reverse)
82
+ expect(monies[0]).to eq(Money.new(82.03))
83
+ expect(monies[1]).to eq(Money.new(36.95))
84
+ expect(monies[2]).to eq(Money.new(369.53)) # <-- leftover penny
85
+
86
+ monies = new_allocator(488.51).allocate([0.05, 0.111, 0.5], :nearest)
87
+ expect(monies[0]).to eq(Money.new(36.95))
88
+ expect(monies[1]).to eq(Money.new(82.04)) # <-- leftover penny
89
+ expect(monies[2]).to eq(Money.new(369.52))
90
+ end
91
+
56
92
  specify "#allocate fills pennies from beginning to end with roundrobin strategy" do
57
- moneys = new_allocator(0.05).allocate([0.3,0.7], :roundrobin)
58
- expect(moneys[0]).to eq(Money.new(0.02))
59
- expect(moneys[1]).to eq(Money.new(0.03))
93
+ #round robin for 1 penny
94
+ monies = new_allocator(0.03).allocate([0.5, 0.5], :roundrobin)
95
+ expect(monies[0]).to eq(Money.new(0.02)) # <-- gets 1 penny
96
+ expect(monies[1]).to eq(Money.new(0.01)) # <-- gets no penny
97
+
98
+ #round robin for 2 pennies
99
+ monies = new_allocator(10.55).allocate([0.25, 0.5, 0.25], :roundrobin)
100
+ expect(monies[0]).to eq(Money.new(2.64)) # <-- gets 1 penny
101
+ expect(monies[1]).to eq(Money.new(5.28)) # <-- gets 1 penny
102
+ expect(monies[2]).to eq(Money.new(2.63)) # <-- gets no penny
103
+
104
+ #round robin for 3 pennies
105
+ monies = new_allocator(195.35).allocate([0.025, 0.025, 0.125, 0.125, 0.4, 0.3], :roundrobin)
106
+ expect(monies[0]).to eq(Money.new(4.89)) # <-- gets 1 penny
107
+ expect(monies[1]).to eq(Money.new(4.89)) # <-- gets 1 penny
108
+ expect(monies[2]).to eq(Money.new(24.42)) # <-- gets 1 penny
109
+ expect(monies[3]).to eq(Money.new(24.41)) # <-- gets no penny
110
+ expect(monies[4]).to eq(Money.new(78.14)) # <-- gets no penny
111
+ expect(monies[5]).to eq(Money.new(58.60)) # <-- gets no penny
112
+
113
+ #round robin for 0 pennies
114
+ monies = new_allocator(101).allocate([0.25, 0.25, 0.25, 0.25], :roundrobin)
115
+ expect(monies[0]).to eq(Money.new(25.25)) # <-- gets no penny
116
+ expect(monies[1]).to eq(Money.new(25.25)) # <-- gets no penny
117
+ expect(monies[2]).to eq(Money.new(25.25)) # <-- gets no penny
118
+ expect(monies[3]).to eq(Money.new(25.25)) # <-- gets no penny
60
119
  end
61
120
 
62
121
  specify "#allocate fills pennies from end to beginning with roundrobin_reverse strategy" do
63
- moneys = new_allocator(0.05).allocate([0.3,0.7], :roundrobin_reverse)
64
- expect(moneys[0]).to eq(Money.new(0.01))
65
- expect(moneys[1]).to eq(Money.new(0.04))
122
+ #round robin reverse for 1 penny
123
+ monies = new_allocator(0.05).allocate([0.3,0.7], :roundrobin_reverse)
124
+ expect(monies[0]).to eq(Money.new(0.01)) # <-- gets no penny
125
+ expect(monies[1]).to eq(Money.new(0.04)) # <-- gets 1 penny
126
+
127
+ #round robin reverse for 2 pennies
128
+ monies = new_allocator(10.55).allocate([0.25, 0.5, 0.25], :roundrobin_reverse)
129
+ expect(monies[0]).to eq(Money.new(2.63)) # <-- gets no penny
130
+ expect(monies[1]).to eq(Money.new(5.28)) # <-- gets 1 penny
131
+ expect(monies[2]).to eq(Money.new(2.64)) # <-- gets 1 penny
132
+
133
+ #round robin reverse for 3 pennies
134
+ monies = new_allocator(195.35).allocate([0.025, 0.025, 0.125, 0.125, 0.4, 0.3], :roundrobin_reverse)
135
+ expect(monies[0]).to eq(Money.new(4.88)) # <-- gets no penny
136
+ expect(monies[1]).to eq(Money.new(4.88)) # <-- gets no penny
137
+ expect(monies[2]).to eq(Money.new(24.41)) # <-- gets no penny
138
+ expect(monies[3]).to eq(Money.new(24.42)) # <-- gets 1 penny
139
+ expect(monies[4]).to eq(Money.new(78.15)) # <-- gets 1 penny
140
+ expect(monies[5]).to eq(Money.new(58.61)) # <-- gets 1 penny
141
+
142
+ #round robin reverse for 0 pennies
143
+ monies = new_allocator(101).allocate([0.25, 0.25, 0.25, 0.25], :roundrobin_reverse)
144
+ expect(monies[0]).to eq(Money.new(25.25)) # <-- gets no penny
145
+ expect(monies[1]).to eq(Money.new(25.25)) # <-- gets no penny
146
+ expect(monies[2]).to eq(Money.new(25.25)) # <-- gets no penny
147
+ expect(monies[3]).to eq(Money.new(25.25)) # <-- gets no penny
66
148
  end
67
149
 
68
- specify "#allocate raise ArgumentError when invalid strategy is provided" do
69
- expect { new_allocator(0.03).allocate([0.5, 0.5], :bad_strategy_name) }.to raise_error(ArgumentError, "Invalid strategy. Valid options: :roundrobin, :roundrobin_reverse")
150
+ specify "#allocate :nearest strategy distributes pennies first to the number which is nearest to the next whole cent" do
151
+ #nearest for 1 penny
152
+ monies = new_allocator(0.03).allocate([0.5, 0.5], :nearest)
153
+ expect(monies[0]).to eq(Money.new(0.02)) # <-- gets 1 penny
154
+ expect(monies[1]).to eq(Money.new(0.01)) # <-- gets no penny
155
+
156
+ #Nearest for 2 pennies
157
+ monies = new_allocator(10.55).allocate([0.25, 0.5, 0.25], :nearest)
158
+ expect(monies[0]).to eq(Money.new(2.64)) # <-- gets 1 penny
159
+ expect(monies[1]).to eq(Money.new(5.27)) # <-- gets no penny
160
+ expect(monies[2]).to eq(Money.new(2.64)) # <-- gets 1 penny
161
+
162
+ #Nearest for 3 pennies
163
+ monies = new_allocator(195.35).allocate([0.025, 0.025, 0.125, 0.125, 0.4, 0.3], :nearest)
164
+ expect(monies[0]).to eq(Money.new(4.88)) # <-- gets no penny
165
+ expect(monies[1]).to eq(Money.new(4.88)) # <-- gets no penny
166
+ expect(monies[2]).to eq(Money.new(24.42)) # <-- gets 1 penny
167
+ expect(monies[3]).to eq(Money.new(24.42)) # <-- gets 1 penny
168
+ expect(monies[4]).to eq(Money.new(78.14)) # <-- gets no penny
169
+ expect(monies[5]).to eq(Money.new(58.61)) # <-- gets 1 penny
170
+
171
+ #Nearest for 0 pennies
172
+ monies = new_allocator(101).allocate([0.25, 0.25, 0.25, 0.25], :nearest)
173
+ expect(monies[0]).to eq(Money.new(25.25)) # <-- gets no penny
174
+ expect(monies[1]).to eq(Money.new(25.25)) # <-- gets no penny
175
+ expect(monies[2]).to eq(Money.new(25.25)) # <-- gets no penny
176
+ expect(monies[3]).to eq(Money.new(25.25)) # <-- gets no penny
70
177
  end
71
178
 
72
- specify "#allocate does not raise ArgumentError when invalid splits types are provided" do
73
- moneys = new_allocator(0.03).allocate([0.5, 0.5], :roundrobin)
74
- expect(moneys[0]).to eq(Money.new(0.02))
75
- expect(moneys[1]).to eq(Money.new(0.01))
179
+ specify "#allocate :roundrobin strategy distributes leftovers from left to right when the currency does not use two decimal places (e.g. `JPY`)" do
180
+ #Roundrobin for 1 yen
181
+ monies = new_allocator(31, 'JPY').allocate([0.5,0.5], :roundrobin)
182
+ expect(monies[0]).to eq(Money.new(16, 'JPY'))
183
+ expect(monies[1]).to eq(Money.new(15, 'JPY'))
184
+
185
+ #Roundrobin for 3 yen
186
+ monies = new_allocator(19535, "JPY").allocate([0.025, 0.025, 0.125, 0.125, 0.4, 0.3], :roundrobin)
187
+ expect(monies[0]).to eq(Money.new(489, "JPY")) # <-- gets 1 yen
188
+ expect(monies[1]).to eq(Money.new(489, "JPY")) # <-- gets 1 yen
189
+ expect(monies[2]).to eq(Money.new(2442, "JPY")) # <-- gets 1 yen
190
+ expect(monies[3]).to eq(Money.new(2441, "JPY")) # <-- gets no yen
191
+ expect(monies[4]).to eq(Money.new(7814, "JPY")) # <-- gets no yen
192
+ expect(monies[5]).to eq(Money.new(5860, "JPY")) # <-- gets no yen
76
193
  end
194
+
195
+ specify "#allocate :roundrobin_reverse strategy distributes leftovers from right to left when the currency does not use two decimal places (e.g. `JPY`)" do
196
+ #Roundrobin for 1 yen
197
+ monies = new_allocator(31, 'JPY').allocate([0.5,0.5], :roundrobin_reverse)
198
+ expect(monies[0]).to eq(Money.new(15, 'JPY'))
199
+ expect(monies[1]).to eq(Money.new(16, 'JPY'))
200
+
201
+ #Roundrobin for 3 yen
202
+ monies = new_allocator(19535, "JPY").allocate([0.025, 0.025, 0.125, 0.125, 0.4, 0.3], :roundrobin_reverse)
203
+ expect(monies[0]).to eq(Money.new(488, "JPY")) # <-- gets no yen
204
+ expect(monies[1]).to eq(Money.new(488, "JPY")) # <-- gets no yen
205
+ expect(monies[2]).to eq(Money.new(2441, "JPY")) # <-- gets no yen
206
+ expect(monies[3]).to eq(Money.new(2442, "JPY")) # <-- gets 1 yen
207
+ expect(monies[4]).to eq(Money.new(7815, "JPY")) # <-- gets 1 yen
208
+ expect(monies[5]).to eq(Money.new(5861, "JPY")) # <-- gets 1 yen
209
+ end
210
+
211
+ specify "#allocate :nearest strategy distributes leftovers to the nearest whole subunity when the currency does not use two decimal places (e.g. `JPY`)" do
212
+ #Nearest for 1 yen
213
+ monies = new_allocator(31, 'JPY').allocate([0.5,0.5], :nearest)
214
+ expect(monies[0]).to eq(Money.new(16, 'JPY'))
215
+ expect(monies[1]).to eq(Money.new(15, 'JPY'))
216
+
217
+ #Nearest for 3 yen
218
+ monies = new_allocator(19535, "JPY").allocate([0.025, 0.025, 0.125, 0.125, 0.4, 0.3], :nearest)
219
+ expect(monies[0]).to eq(Money.new(488, "JPY")) # <-- gets no yen
220
+ expect(monies[1]).to eq(Money.new(488, "JPY")) # <-- gets no yen
221
+ expect(monies[2]).to eq(Money.new(2442, "JPY")) # <-- gets 1 yen
222
+ expect(monies[3]).to eq(Money.new(2442, "JPY")) # <-- gets 1 yen
223
+ expect(monies[4]).to eq(Money.new(7814, "JPY")) # <-- gets no yen
224
+ expect(monies[5]).to eq(Money.new(5861, "JPY")) # <-- gets 1 yen
225
+ end
226
+
77
227
  end
78
228
 
79
229
  describe 'allocate_max_amounts' do
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
 
4
4
  RSpec.describe "deprecations" do
5
5
  it "has the deprecation_horizon as the next major release" do
6
- allow(Money).to receive(:const_get).with('VERSION').and_return("2.0.0")
6
+ allow(Money).to receive(:const_get).with('VERSION').and_return("2.1.0")
7
7
  expect(Money.active_support_deprecator.deprecation_horizon).to eq("3.0.0")
8
8
  end
9
9
  end
data/spec/helpers_spec.rb CHANGED
@@ -7,8 +7,8 @@ RSpec.describe Money::Helpers do
7
7
  let (:amount) { BigDecimal('1.23') }
8
8
  let (:money) { Money.new(amount) }
9
9
 
10
- it 'raises when provided with a money object' do
11
- expect { subject.value_to_decimal(money) }.to raise_error(ArgumentError)
10
+ it 'returns the value of a money object' do
11
+ expect(subject.value_to_decimal(money)).to eq(amount)
12
12
  end
13
13
 
14
14
  it 'returns itself if it is already a big decimal' do