shopify-money 2.0.0 → 2.2.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.
@@ -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