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.
- checksums.yaml +4 -4
- data/.gitignore +1 -2
- data/.ruby-version +1 -0
- data/Gemfile.lock +234 -0
- data/README.md +26 -1
- data/dev.yml +1 -1
- data/lib/money/allocator.rb +57 -25
- data/lib/money/helpers.rb +2 -0
- data/lib/money/money.rb +70 -47
- data/lib/money/splitter.rb +115 -0
- data/lib/money/version.rb +1 -1
- data/lib/money.rb +1 -0
- data/money.gemspec +1 -1
- data/spec/allocator_spec.rb +175 -25
- data/spec/deprecations_spec.rb +1 -1
- data/spec/helpers_spec.rb +2 -2
- data/spec/money_spec.rb +79 -6
- data/spec/splitter_spec.rb +104 -0
- metadata +12 -7
@@ -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
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"
|
23
|
+
s.add_development_dependency("sqlite3")
|
24
24
|
|
25
25
|
s.required_ruby_version = '>= 3.0'
|
26
26
|
|
data/spec/allocator_spec.rb
CHANGED
@@ -8,34 +8,38 @@ RSpec.describe "Allocator" do
|
|
8
8
|
end
|
9
9
|
|
10
10
|
specify "#allocate does not lose pennies" do
|
11
|
-
|
12
|
-
expect(
|
13
|
-
expect(
|
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
|
-
|
18
|
-
expect(
|
19
|
-
expect(
|
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
|
-
|
24
|
-
expect(
|
25
|
-
expect(
|
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
|
-
|
30
|
-
expect(
|
31
|
-
expect(
|
32
|
-
expect(
|
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
|
-
|
58
|
-
|
59
|
-
expect(
|
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
|
-
|
64
|
-
|
65
|
-
expect(
|
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
|
69
|
-
|
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
|
73
|
-
|
74
|
-
|
75
|
-
expect(
|
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
|
data/spec/deprecations_spec.rb
CHANGED
@@ -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.
|
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 '
|
11
|
-
expect
|
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
|