shopify-money 2.0.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.0"
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'
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
data/spec/money_spec.rb CHANGED
@@ -30,15 +30,20 @@ RSpec.describe "Money" do
30
30
 
31
31
  it "returns itself with to_money" do
32
32
  expect(money.to_money).to eq(money)
33
+ expect(amount_money.to_money).to eq(amount_money)
33
34
  end
34
35
 
35
36
  it "#to_money uses the provided currency when it doesn't already have one" do
36
37
  expect(Money.new(1).to_money('CAD')).to eq(Money.new(1, 'CAD'))
37
38
  end
38
39
 
40
+ it "#to_money works with money objects of the same currency" do
41
+ expect(Money.new(1, 'CAD').to_money('CAD')).to eq(Money.new(1, 'CAD'))
42
+ end
43
+
39
44
  it "legacy_deprecations #to_money doesn't overwrite the money object's currency" do
40
45
  configure(legacy_deprecations: true) do
41
- expect(Money).to receive(:deprecate).once
46
+ expect(Money).to receive(:deprecate).with(match(/to_money is attempting to change currency of an existing money object/)).once
42
47
  expect(Money.new(1, 'USD').to_money('CAD')).to eq(Money.new(1, 'USD'))
43
48
  end
44
49
  end
@@ -55,6 +60,29 @@ RSpec.describe "Money" do
55
60
  expect(Money.new('')).to eq(Money.new(0))
56
61
  end
57
62
 
63
+ it "can be constructed with a string" do
64
+ expect(Money.new('1')).to eq(Money.new(1))
65
+ end
66
+
67
+ it "can be constructed with a numeric" do
68
+ expect(Money.new(1.00)).to eq(Money.new(1))
69
+ end
70
+
71
+ it "can be constructed with a money object" do
72
+ expect(Money.new(Money.new(1))).to eq(Money.new(1))
73
+ end
74
+
75
+ it "constructor raises when changing currency" do
76
+ expect { Money.new(Money.new(1, 'USD'), 'CAD') }.to raise_error(Money::IncompatibleCurrencyError)
77
+ end
78
+
79
+ it "legacy_deprecations constructor with money used the constructor currency" do
80
+ configure(legacy_deprecations: true) do
81
+ expect(Money).to receive(:deprecate).with(match(/Money.new is attempting to change currency of an existing money object/)).once
82
+ expect(Money.new(Money.new(1, 'USD'), 'CAD')).to eq(Money.new(1, 'CAD'))
83
+ end
84
+ end
85
+
58
86
  it "legacy_deprecations defaults to 0 when constructed with an invalid string" do
59
87
  configure(legacy_deprecations: true) do
60
88
  expect(Money).to receive(:deprecate).once
@@ -192,7 +220,7 @@ RSpec.describe "Money" do
192
220
 
193
221
  it "logs a deprecation warning when adding across currencies" do
194
222
  configure(legacy_deprecations: true) do
195
- expect(Money).to receive(:deprecate)
223
+ expect(Money).to receive(:deprecate).with(match(/mathematical operation not permitted for Money objects with different currencies/))
196
224
  expect(Money.new(10, 'USD') - Money.new(1, 'JPY')).to eq(Money.new(9, 'USD'))
197
225
  end
198
226
  end
@@ -723,22 +751,32 @@ RSpec.describe "Money" do
723
751
  specify "#split needs at least one party" do
724
752
  expect {Money.new(1).split(0)}.to raise_error(ArgumentError)
725
753
  expect {Money.new(1).split(-1)}.to raise_error(ArgumentError)
754
+ expect {Money.new(1).split(0.1)}.to raise_error(ArgumentError)
755
+ expect(Money.new(1).split(BigDecimal("0.1e1")).to_a).to eq([Money.new(1)])
756
+ end
757
+
758
+ specify "#split can be zipped" do
759
+ expect(Money.new(100).split(3).zip(Money.new(50).split(3)).to_a).to eq([
760
+ [Money.new(33.34), Money.new(16.67)],
761
+ [Money.new(33.33), Money.new(16.67)],
762
+ [Money.new(33.33), Money.new(16.66)],
763
+ ])
726
764
  end
727
765
 
728
766
  specify "#gives 1 cent to both people if we start with 2" do
729
- expect(Money.new(0.02).split(2)).to eq([Money.new(0.01), Money.new(0.01)])
767
+ expect(Money.new(0.02).split(2).to_a).to eq([Money.new(0.01), Money.new(0.01)])
730
768
  end
731
769
 
732
770
  specify "#split may distribute no money to some parties if there isnt enough to go around" do
733
- expect(Money.new(0.02).split(3)).to eq([Money.new(0.01), Money.new(0.01), Money.new(0)])
771
+ expect(Money.new(0.02).split(3).to_a).to eq([Money.new(0.01), Money.new(0.01), Money.new(0)])
734
772
  end
735
773
 
736
774
  specify "#split does not lose pennies" do
737
- expect(Money.new(0.05).split(2)).to eq([Money.new(0.03), Money.new(0.02)])
775
+ expect(Money.new(0.05).split(2).to_a).to eq([Money.new(0.03), Money.new(0.02)])
738
776
  end
739
777
 
740
778
  specify "#split does not lose dollars with non-decimal currencies" do
741
- expect(Money.new(5, 'JPY').split(2)).to eq([Money.new(3, 'JPY'), Money.new(2, 'JPY')])
779
+ expect(Money.new(5, 'JPY').split(2).to_a).to eq([Money.new(3, 'JPY'), Money.new(2, 'JPY')])
742
780
  end
743
781
 
744
782
  specify "#split a dollar" do
@@ -754,6 +792,41 @@ RSpec.describe "Money" do
754
792
  expect(moneys[1].value).to eq(33)
755
793
  expect(moneys[2].value).to eq(33)
756
794
  end
795
+
796
+ specify "#split return respond to #first" do
797
+ expect(Money.new(100).split(3).first).to eq(Money.new(33.34))
798
+ expect(Money.new(100).split(3).first(2)).to eq([Money.new(33.34), Money.new(33.33)])
799
+
800
+ expect(Money.new(100).split(10).first).to eq(Money.new(10))
801
+ expect(Money.new(100).split(10).first(2)).to eq([Money.new(10), Money.new(10)])
802
+ expect(Money.new(20).split(2).first(4)).to eq([Money.new(10), Money.new(10)])
803
+ end
804
+
805
+ specify "#split return respond to #last" do
806
+ expect(Money.new(100).split(3).last).to eq(Money.new(33.33))
807
+ expect(Money.new(100).split(3).last(2)).to eq([Money.new(33.33), Money.new(33.33)])
808
+ expect(Money.new(20).split(2).last(4)).to eq([Money.new(10), Money.new(10)])
809
+ end
810
+
811
+ specify "#split return supports destructuring" do
812
+ first, second = Money.new(100).split(3)
813
+ expect(first).to eq(Money.new(33.34))
814
+ expect(second).to eq(Money.new(33.33))
815
+
816
+ first, *rest = Money.new(100).split(3)
817
+ expect(first).to eq(Money.new(33.34))
818
+ expect(rest).to eq([Money.new(33.33), Money.new(33.33)])
819
+ end
820
+
821
+ specify "#split return can be reversed" do
822
+ list = Money.new(100).split(3)
823
+ expect(list.first).to eq(Money.new(33.34))
824
+ expect(list.last).to eq(Money.new(33.33))
825
+
826
+ list = list.reverse
827
+ expect(list.first).to eq(Money.new(33.33))
828
+ expect(list.last).to eq(Money.new(33.34))
829
+ end
757
830
  end
758
831
 
759
832
  describe "calculate_splits" do
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'yaml'
4
+
5
+ RSpec.describe "Money::Splitter" do
6
+ specify "#split needs at least one party" do
7
+ expect {Money.new(1).split(0)}.to raise_error(ArgumentError)
8
+ expect {Money.new(1).split(-1)}.to raise_error(ArgumentError)
9
+ expect {Money.new(1).split(0.1)}.to raise_error(ArgumentError)
10
+ expect(Money.new(1).split(BigDecimal("0.1e1")).to_a).to eq([Money.new(1)])
11
+ end
12
+
13
+ specify "#split can be zipped" do
14
+ expect(Money.new(100).split(3).zip(Money.new(50).split(3)).to_a).to eq([
15
+ [Money.new(33.34), Money.new(16.67)],
16
+ [Money.new(33.33), Money.new(16.67)],
17
+ [Money.new(33.33), Money.new(16.66)],
18
+ ])
19
+ end
20
+
21
+ specify "#gives 1 cent to both people if we start with 2" do
22
+ expect(Money.new(0.02).split(2).to_a).to eq([Money.new(0.01), Money.new(0.01)])
23
+ end
24
+
25
+ specify "#split may distribute no money to some parties if there isnt enough to go around" do
26
+ expect(Money.new(0.02).split(3).to_a).to eq([Money.new(0.01), Money.new(0.01), Money.new(0)])
27
+ end
28
+
29
+ specify "#split does not lose pennies" do
30
+ expect(Money.new(0.05).split(2).to_a).to eq([Money.new(0.03), Money.new(0.02)])
31
+ end
32
+
33
+ specify "#split does not lose dollars with non-decimal currencies" do
34
+ expect(Money.new(5, 'JPY').split(2).to_a).to eq([Money.new(3, 'JPY'), Money.new(2, 'JPY')])
35
+ end
36
+
37
+ specify "#split a dollar" do
38
+ moneys = Money.new(1).split(3)
39
+ expect(moneys[0].subunits).to eq(34)
40
+ expect(moneys[1].subunits).to eq(33)
41
+ expect(moneys[2].subunits).to eq(33)
42
+ end
43
+
44
+ specify "#split a 100 yen" do
45
+ moneys = Money.new(100, 'JPY').split(3)
46
+ expect(moneys[0].value).to eq(34)
47
+ expect(moneys[1].value).to eq(33)
48
+ expect(moneys[2].value).to eq(33)
49
+ end
50
+
51
+ specify "#split return respond to #first" do
52
+ expect(Money.new(100).split(3).first).to eq(Money.new(33.34))
53
+ expect(Money.new(100).split(3).first(2)).to eq([Money.new(33.34), Money.new(33.33)])
54
+
55
+ expect(Money.new(100).split(10).first).to eq(Money.new(10))
56
+ expect(Money.new(100).split(10).first(2)).to eq([Money.new(10), Money.new(10)])
57
+ expect(Money.new(20).split(2).first(4)).to eq([Money.new(10), Money.new(10)])
58
+ end
59
+
60
+ specify "#split return respond to #last" do
61
+ expect(Money.new(100).split(3).last).to eq(Money.new(33.33))
62
+ expect(Money.new(100).split(3).last(2)).to eq([Money.new(33.33), Money.new(33.33)])
63
+ expect(Money.new(20).split(2).last(4)).to eq([Money.new(10), Money.new(10)])
64
+ end
65
+
66
+ specify "#split return supports destructuring" do
67
+ first, second = Money.new(100).split(3)
68
+ expect(first).to eq(Money.new(33.34))
69
+ expect(second).to eq(Money.new(33.33))
70
+
71
+ first, *rest = Money.new(100).split(3)
72
+ expect(first).to eq(Money.new(33.34))
73
+ expect(rest).to eq([Money.new(33.33), Money.new(33.33)])
74
+ end
75
+
76
+ specify "#split return can be reversed" do
77
+ list = Money.new(100).split(3)
78
+ expect(list.first).to eq(Money.new(33.34))
79
+ expect(list.last).to eq(Money.new(33.33))
80
+
81
+ list = list.reverse
82
+ expect(list.first).to eq(Money.new(33.33))
83
+ expect(list.last).to eq(Money.new(33.34))
84
+ end
85
+
86
+ describe "calculate_splits" do
87
+ specify "#calculate_splits gives 1 cent to both people if we start with 2" do
88
+ actual = Money.new(0.02, 'CAD').calculate_splits(2)
89
+
90
+ expect(actual).to eq({
91
+ Money.new(0.01, 'CAD') => 2,
92
+ })
93
+ end
94
+
95
+ specify "#calculate_splits gives an extra penny to one" do
96
+ actual = Money.new(0.04, 'CAD').calculate_splits(3)
97
+
98
+ expect(actual).to eq({
99
+ Money.new(0.02, 'CAD') => 1,
100
+ Money.new(0.01, 'CAD') => 2,
101
+ })
102
+ end
103
+ end
104
+ end