shopify-money 2.0.0 → 2.2.1

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