shopify-money 1.3.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/.github/workflows/tests.yml +2 -2
- data/.gitignore +1 -2
- data/.ruby-version +1 -0
- data/Gemfile.lock +234 -0
- data/README.md +48 -6
- data/dev.yml +1 -1
- data/lib/money/allocator.rb +57 -25
- data/lib/money/money.rb +72 -54
- data/lib/money/splitter.rb +115 -0
- data/lib/money/version.rb +1 -1
- data/lib/money.rb +1 -0
- data/money.gemspec +2 -2
- data/spec/allocator_spec.rb +175 -25
- data/spec/deprecations_spec.rb +2 -2
- data/spec/money_spec.rb +79 -13
- data/spec/splitter_spec.rb +104 -0
- metadata +13 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 75ba2eb41fa1e50dac837fab61774cdb4a05407b030cb7ba2989b60de27bbfe4
|
|
4
|
+
data.tar.gz: e612602f0f07fe052e6a2e53046e7b6b9a05c56775a75c6a3e4d1f7984671a49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ffa8231621bc6d26f446425cab725e4a3fdcef5a5ec1c7bc240f6397c6fd07514863b56a44c89b413c4750f4a20eb8b218500ea2ab5c0120b7c158f5a24b9a46
|
|
7
|
+
data.tar.gz: 6b9582714d55a4e1150b01fcb3eada86751f6408b943811570470c88fda04a7e9db8fdc8a1da65ff46bdf465eb641be521ac474fe1ec6dcc952598d6c32594c0
|
data/.github/workflows/tests.yml
CHANGED
data/.gitignore
CHANGED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.3.0
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
shopify-money (2.2.0)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
actioncable (6.1.7.7)
|
|
10
|
+
actionpack (= 6.1.7.7)
|
|
11
|
+
activesupport (= 6.1.7.7)
|
|
12
|
+
nio4r (~> 2.0)
|
|
13
|
+
websocket-driver (>= 0.6.1)
|
|
14
|
+
actionmailbox (6.1.7.7)
|
|
15
|
+
actionpack (= 6.1.7.7)
|
|
16
|
+
activejob (= 6.1.7.7)
|
|
17
|
+
activerecord (= 6.1.7.7)
|
|
18
|
+
activestorage (= 6.1.7.7)
|
|
19
|
+
activesupport (= 6.1.7.7)
|
|
20
|
+
mail (>= 2.7.1)
|
|
21
|
+
actionmailer (6.1.7.7)
|
|
22
|
+
actionpack (= 6.1.7.7)
|
|
23
|
+
actionview (= 6.1.7.7)
|
|
24
|
+
activejob (= 6.1.7.7)
|
|
25
|
+
activesupport (= 6.1.7.7)
|
|
26
|
+
mail (~> 2.5, >= 2.5.4)
|
|
27
|
+
rails-dom-testing (~> 2.0)
|
|
28
|
+
actionpack (6.1.7.7)
|
|
29
|
+
actionview (= 6.1.7.7)
|
|
30
|
+
activesupport (= 6.1.7.7)
|
|
31
|
+
rack (~> 2.0, >= 2.0.9)
|
|
32
|
+
rack-test (>= 0.6.3)
|
|
33
|
+
rails-dom-testing (~> 2.0)
|
|
34
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
|
35
|
+
actiontext (6.1.7.7)
|
|
36
|
+
actionpack (= 6.1.7.7)
|
|
37
|
+
activerecord (= 6.1.7.7)
|
|
38
|
+
activestorage (= 6.1.7.7)
|
|
39
|
+
activesupport (= 6.1.7.7)
|
|
40
|
+
nokogiri (>= 1.8.5)
|
|
41
|
+
actionview (6.1.7.7)
|
|
42
|
+
activesupport (= 6.1.7.7)
|
|
43
|
+
builder (~> 3.1)
|
|
44
|
+
erubi (~> 1.4)
|
|
45
|
+
rails-dom-testing (~> 2.0)
|
|
46
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
|
47
|
+
activejob (6.1.7.7)
|
|
48
|
+
activesupport (= 6.1.7.7)
|
|
49
|
+
globalid (>= 0.3.6)
|
|
50
|
+
activemodel (6.1.7.7)
|
|
51
|
+
activesupport (= 6.1.7.7)
|
|
52
|
+
activerecord (6.1.7.7)
|
|
53
|
+
activemodel (= 6.1.7.7)
|
|
54
|
+
activesupport (= 6.1.7.7)
|
|
55
|
+
activestorage (6.1.7.7)
|
|
56
|
+
actionpack (= 6.1.7.7)
|
|
57
|
+
activejob (= 6.1.7.7)
|
|
58
|
+
activerecord (= 6.1.7.7)
|
|
59
|
+
activesupport (= 6.1.7.7)
|
|
60
|
+
marcel (~> 1.0)
|
|
61
|
+
mini_mime (>= 1.1.0)
|
|
62
|
+
activesupport (6.1.7.7)
|
|
63
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
64
|
+
i18n (>= 1.6, < 2)
|
|
65
|
+
minitest (>= 5.1)
|
|
66
|
+
tzinfo (~> 2.0)
|
|
67
|
+
zeitwerk (~> 2.3)
|
|
68
|
+
ast (2.4.2)
|
|
69
|
+
builder (3.2.4)
|
|
70
|
+
byebug (11.1.3)
|
|
71
|
+
coderay (1.1.3)
|
|
72
|
+
concurrent-ruby (1.2.3)
|
|
73
|
+
crass (1.0.6)
|
|
74
|
+
database_cleaner (1.99.0)
|
|
75
|
+
date (3.3.4)
|
|
76
|
+
diff-lcs (1.5.1)
|
|
77
|
+
docile (1.4.0)
|
|
78
|
+
erubi (1.12.0)
|
|
79
|
+
globalid (1.2.1)
|
|
80
|
+
activesupport (>= 6.1)
|
|
81
|
+
i18n (1.14.4)
|
|
82
|
+
concurrent-ruby (~> 1.0)
|
|
83
|
+
jaro_winkler (1.5.6)
|
|
84
|
+
loofah (2.22.0)
|
|
85
|
+
crass (~> 1.0.2)
|
|
86
|
+
nokogiri (>= 1.12.0)
|
|
87
|
+
mail (2.8.1)
|
|
88
|
+
mini_mime (>= 0.1.1)
|
|
89
|
+
net-imap
|
|
90
|
+
net-pop
|
|
91
|
+
net-smtp
|
|
92
|
+
marcel (1.0.4)
|
|
93
|
+
method_source (1.0.0)
|
|
94
|
+
mini_mime (1.1.5)
|
|
95
|
+
minitest (5.22.3)
|
|
96
|
+
net-imap (0.4.10)
|
|
97
|
+
date
|
|
98
|
+
net-protocol
|
|
99
|
+
net-pop (0.1.2)
|
|
100
|
+
net-protocol
|
|
101
|
+
net-protocol (0.2.2)
|
|
102
|
+
timeout
|
|
103
|
+
net-smtp (0.5.0)
|
|
104
|
+
net-protocol
|
|
105
|
+
nio4r (2.7.1)
|
|
106
|
+
nokogiri (1.16.3-aarch64-linux)
|
|
107
|
+
racc (~> 1.4)
|
|
108
|
+
nokogiri (1.16.3-arm-linux)
|
|
109
|
+
racc (~> 1.4)
|
|
110
|
+
nokogiri (1.16.3-arm64-darwin)
|
|
111
|
+
racc (~> 1.4)
|
|
112
|
+
nokogiri (1.16.3-x86-linux)
|
|
113
|
+
racc (~> 1.4)
|
|
114
|
+
nokogiri (1.16.3-x86_64-darwin)
|
|
115
|
+
racc (~> 1.4)
|
|
116
|
+
nokogiri (1.16.3-x86_64-linux)
|
|
117
|
+
racc (~> 1.4)
|
|
118
|
+
parallel (1.24.0)
|
|
119
|
+
parser (3.3.0.5)
|
|
120
|
+
ast (~> 2.4.1)
|
|
121
|
+
racc
|
|
122
|
+
pry (0.14.2)
|
|
123
|
+
coderay (~> 1.1)
|
|
124
|
+
method_source (~> 1.0)
|
|
125
|
+
pry-byebug (3.10.1)
|
|
126
|
+
byebug (~> 11.0)
|
|
127
|
+
pry (>= 0.13, < 0.15)
|
|
128
|
+
racc (1.7.3)
|
|
129
|
+
rack (2.2.9)
|
|
130
|
+
rack-test (2.1.0)
|
|
131
|
+
rack (>= 1.3)
|
|
132
|
+
rails (6.1.7.7)
|
|
133
|
+
actioncable (= 6.1.7.7)
|
|
134
|
+
actionmailbox (= 6.1.7.7)
|
|
135
|
+
actionmailer (= 6.1.7.7)
|
|
136
|
+
actionpack (= 6.1.7.7)
|
|
137
|
+
actiontext (= 6.1.7.7)
|
|
138
|
+
actionview (= 6.1.7.7)
|
|
139
|
+
activejob (= 6.1.7.7)
|
|
140
|
+
activemodel (= 6.1.7.7)
|
|
141
|
+
activerecord (= 6.1.7.7)
|
|
142
|
+
activestorage (= 6.1.7.7)
|
|
143
|
+
activesupport (= 6.1.7.7)
|
|
144
|
+
bundler (>= 1.15.0)
|
|
145
|
+
railties (= 6.1.7.7)
|
|
146
|
+
sprockets-rails (>= 2.0.0)
|
|
147
|
+
rails-dom-testing (2.2.0)
|
|
148
|
+
activesupport (>= 5.0.0)
|
|
149
|
+
minitest
|
|
150
|
+
nokogiri (>= 1.6)
|
|
151
|
+
rails-html-sanitizer (1.6.0)
|
|
152
|
+
loofah (~> 2.21)
|
|
153
|
+
nokogiri (~> 1.14)
|
|
154
|
+
railties (6.1.7.7)
|
|
155
|
+
actionpack (= 6.1.7.7)
|
|
156
|
+
activesupport (= 6.1.7.7)
|
|
157
|
+
method_source
|
|
158
|
+
rake (>= 12.2)
|
|
159
|
+
thor (~> 1.0)
|
|
160
|
+
rainbow (3.1.1)
|
|
161
|
+
rake (13.2.0)
|
|
162
|
+
rexml (3.2.6)
|
|
163
|
+
rspec (3.13.0)
|
|
164
|
+
rspec-core (~> 3.13.0)
|
|
165
|
+
rspec-expectations (~> 3.13.0)
|
|
166
|
+
rspec-mocks (~> 3.13.0)
|
|
167
|
+
rspec-core (3.13.0)
|
|
168
|
+
rspec-support (~> 3.13.0)
|
|
169
|
+
rspec-expectations (3.13.0)
|
|
170
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
171
|
+
rspec-support (~> 3.13.0)
|
|
172
|
+
rspec-mocks (3.13.0)
|
|
173
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
174
|
+
rspec-support (~> 3.13.0)
|
|
175
|
+
rspec-support (3.13.1)
|
|
176
|
+
rubocop (0.81.0)
|
|
177
|
+
jaro_winkler (~> 1.5.1)
|
|
178
|
+
parallel (~> 1.10)
|
|
179
|
+
parser (>= 2.7.0.1)
|
|
180
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
181
|
+
rexml
|
|
182
|
+
ruby-progressbar (~> 1.7)
|
|
183
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
|
184
|
+
ruby-progressbar (1.13.0)
|
|
185
|
+
simplecov (0.22.0)
|
|
186
|
+
docile (~> 1.1)
|
|
187
|
+
simplecov-html (~> 0.11)
|
|
188
|
+
simplecov_json_formatter (~> 0.1)
|
|
189
|
+
simplecov-html (0.12.3)
|
|
190
|
+
simplecov_json_formatter (0.1.4)
|
|
191
|
+
sprockets (4.2.1)
|
|
192
|
+
concurrent-ruby (~> 1.0)
|
|
193
|
+
rack (>= 2.2.4, < 4)
|
|
194
|
+
sprockets-rails (3.4.2)
|
|
195
|
+
actionpack (>= 5.2)
|
|
196
|
+
activesupport (>= 5.2)
|
|
197
|
+
sprockets (>= 3.0.0)
|
|
198
|
+
sqlite3 (1.7.3-aarch64-linux)
|
|
199
|
+
sqlite3 (1.7.3-arm-linux)
|
|
200
|
+
sqlite3 (1.7.3-arm64-darwin)
|
|
201
|
+
sqlite3 (1.7.3-x86-linux)
|
|
202
|
+
sqlite3 (1.7.3-x86_64-darwin)
|
|
203
|
+
sqlite3 (1.7.3-x86_64-linux)
|
|
204
|
+
thor (1.3.1)
|
|
205
|
+
timeout (0.4.1)
|
|
206
|
+
tzinfo (2.0.6)
|
|
207
|
+
concurrent-ruby (~> 1.0)
|
|
208
|
+
unicode-display_width (1.8.0)
|
|
209
|
+
websocket-driver (0.7.6)
|
|
210
|
+
websocket-extensions (>= 0.1.0)
|
|
211
|
+
websocket-extensions (0.1.5)
|
|
212
|
+
zeitwerk (2.6.13)
|
|
213
|
+
|
|
214
|
+
PLATFORMS
|
|
215
|
+
aarch64-linux
|
|
216
|
+
arm-linux
|
|
217
|
+
arm64-darwin
|
|
218
|
+
x86-linux
|
|
219
|
+
x86_64-darwin
|
|
220
|
+
x86_64-linux
|
|
221
|
+
|
|
222
|
+
DEPENDENCIES
|
|
223
|
+
bundler
|
|
224
|
+
database_cleaner (~> 1.6)
|
|
225
|
+
pry-byebug
|
|
226
|
+
rails (~> 6.0)
|
|
227
|
+
rspec (~> 3.2)
|
|
228
|
+
rubocop (~> 0.81.0)
|
|
229
|
+
shopify-money!
|
|
230
|
+
simplecov
|
|
231
|
+
sqlite3
|
|
232
|
+
|
|
233
|
+
BUNDLED WITH
|
|
234
|
+
2.5.3
|
data/README.md
CHANGED
|
@@ -11,7 +11,8 @@ money_column expects a DECIMAL(21,3) database field.
|
|
|
11
11
|
- Provides a `Money::Currency` class which encapsulates all information about a monetary unit.
|
|
12
12
|
- Represents monetary values as decimals. No need to convert your amounts every time you use them. Easily understand the data in your DB.
|
|
13
13
|
- Does NOT provide APIs for exchanging money from one currency to another.
|
|
14
|
-
- Will not lose pennies during divisions
|
|
14
|
+
- Will not lose pennies during divisions. For instance, given $1 / 3 the resulting chunks will be .34, .33, and .33. Notice that one chunk is larger than the others, so the result still adds to $1.
|
|
15
|
+
- Allows callers to select a rounding strategy when dividing, to determine the order in which leftover pennies are given out.
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
17
18
|
|
|
@@ -29,7 +30,7 @@ require 'money'
|
|
|
29
30
|
# 10.00 USD
|
|
30
31
|
money = Money.new(10.00, "USD")
|
|
31
32
|
money.subunits #=> 1000
|
|
32
|
-
money.currency
|
|
33
|
+
money.currency #=> Money::Currency.new("USD")
|
|
33
34
|
|
|
34
35
|
# Comparisons
|
|
35
36
|
Money.new(1000, "USD") == Money.new(1000, "USD") #=> true
|
|
@@ -40,13 +41,54 @@ Money.new(1000, "USD") != Money.new(1000, "EUR") #=> true
|
|
|
40
41
|
# Arithmetic
|
|
41
42
|
Money.new(1000, "USD") + Money.new(500, "USD") == Money.new(1500, "USD")
|
|
42
43
|
Money.new(1000, "USD") - Money.new(200, "USD") == Money.new(800, "USD")
|
|
43
|
-
Money.new(1000, "USD") / 5 == Money.new(200, "USD")
|
|
44
44
|
Money.new(1000, "USD") * 5 == Money.new(5000, "USD")
|
|
45
45
|
|
|
46
|
+
m = Money.new(1000, "USD")
|
|
47
|
+
# Splitting money evenly
|
|
48
|
+
m.split(2) == [Money.new(500, "USD"), Money.new(500, "USD")]
|
|
49
|
+
m.split(3).map(&:value) == [333.34, 333.33, 333.33]
|
|
50
|
+
m.calculate_splits(2) == { Money.new(500, "USD") => 2 }
|
|
51
|
+
m.calculate_splits(3) == { Money.new(333.34, "USD") => 1, Money.new(333.33, "USD") =>2 }
|
|
52
|
+
|
|
53
|
+
# Allocating money proportionally
|
|
54
|
+
m.allocate([0.50, 0.25, 0.25]).map(&:value) == [500, 250, 250]
|
|
55
|
+
m.allocate([Rational(2, 3), Rational(1, 3)]).map(&:value) == [666.67, 333.33]
|
|
56
|
+
|
|
57
|
+
## Allocating up to a cutoff
|
|
58
|
+
m.allocate_max_amounts([500, 300, 200]).map(&:value) == [500, 300, 200]
|
|
59
|
+
m.allocate_max_amounts([500, 300, 300]).map(&:value) == [454.55, 272.73, 272.72]
|
|
60
|
+
|
|
61
|
+
## Selectable rounding strategies during division
|
|
62
|
+
|
|
63
|
+
# Assigns leftover subunits left to right
|
|
64
|
+
m = Money::Allocator.new(Money.new(10.55, "USD"))
|
|
65
|
+
monies = m.allocate([0.25, 0.5, 0.25], :roundrobin)
|
|
66
|
+
#monies[0] == 2.64 <-- gets 1 penny
|
|
67
|
+
#monies[1] == 5.28 <-- gets 1 penny
|
|
68
|
+
#monies[2] == 2.63 <-- gets no penny
|
|
69
|
+
|
|
70
|
+
# Assigns leftover subunits right to left
|
|
71
|
+
m = Money::Allocator.new(Money.new(10.55, "USD"))
|
|
72
|
+
monies = m.allocate([0.25, 0.5, 0.25], :roundrobin_reverse)
|
|
73
|
+
#monies[0] == 2.63 <-- gets no penny
|
|
74
|
+
#monies[1] == 5.28 <-- gets 1 penny
|
|
75
|
+
#monies[2] == 2.64 <-- gets 1 penny
|
|
76
|
+
|
|
77
|
+
# Assigns leftover subunits to the nearest whole subunit
|
|
78
|
+
m = Money::Allocator.new(Money.new(10.55, "USD"))
|
|
79
|
+
monies = m.allocate([0.25, 0.5, 0.25], :nearest)
|
|
80
|
+
#monies[0] == 2.64 <-- gets 1 penny
|
|
81
|
+
#monies[1] == 5.27 <-- gets no penny
|
|
82
|
+
#monies[2] == 2.64 <-- gets 1 penny
|
|
83
|
+
# $2.6375 is closer to the next whole penny than $5.275
|
|
84
|
+
|
|
85
|
+
# Clamp
|
|
86
|
+
Money.new(50, "USD").clamp(1, 100) == Money.new(50, "USD")
|
|
87
|
+
|
|
46
88
|
# Unit to subunit conversions
|
|
47
|
-
Money.from_subunits(500, "USD")
|
|
48
|
-
Money.from_subunits(5, "JPY")
|
|
49
|
-
Money.from_subunits(5000, "TND") == Money.new(5, "TND")
|
|
89
|
+
Money.from_subunits(500, "USD") == Money.new(5, "USD") # 5 USD
|
|
90
|
+
Money.from_subunits(5, "JPY") == Money.new(5, "JPY") # 5 JPY
|
|
91
|
+
Money.from_subunits(5000, "TND") == Money.new(5, "TND") # 5 TND
|
|
50
92
|
```
|
|
51
93
|
|
|
52
94
|
## Currency
|
data/dev.yml
CHANGED
data/lib/money/allocator.rb
CHANGED
|
@@ -9,18 +9,27 @@ class Money
|
|
|
9
9
|
|
|
10
10
|
ONE = BigDecimal("1")
|
|
11
11
|
|
|
12
|
-
# Allocates money between different parties without losing
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
12
|
+
# Allocates money between different parties without losing subunits. A "subunit"
|
|
13
|
+
# in this context is the smallest unit of a currency that can be divided no
|
|
14
|
+
# further. In USD the unit is dollars and the subunit is cents. In JPY the unit
|
|
15
|
+
# is yen and the subunit is also yen. So given $1 divided by 3, the resulting subunits
|
|
16
|
+
# should be [34¢, 33¢, 33¢]. Notice that one of these chunks is larger than the other
|
|
17
|
+
# two, because we cannot transact in amounts less than 1 subunit.
|
|
18
|
+
#
|
|
19
|
+
# After the mathematically split has been performed, left over subunits will
|
|
20
|
+
# be distributed round-robin or nearest-subunit strategy amongst the parties.
|
|
21
|
+
# Round-robin strategy has the virtue of being easier to understand, while
|
|
22
|
+
# nearest-subunit is a more complex alogirthm that results in the most fair
|
|
23
|
+
# distribution.
|
|
16
24
|
#
|
|
17
25
|
# @param splits [Array<Numeric>]
|
|
18
26
|
# @param strategy Symbol
|
|
19
27
|
# @return [Array<Money>]
|
|
20
28
|
#
|
|
21
29
|
# Strategies:
|
|
22
|
-
# - `:roundrobin` (default): leftover
|
|
23
|
-
# - `:roundrobin_reverse`: leftover
|
|
30
|
+
# - `:roundrobin` (default): leftover subunits will be accumulated starting from the first allocation left to right
|
|
31
|
+
# - `:roundrobin_reverse`: leftover subunits will be accumulated starting from the last allocation right to left
|
|
32
|
+
# - `:nearest`: leftover subunits will by given first to the party closest to the next whole subunit
|
|
24
33
|
#
|
|
25
34
|
# @example
|
|
26
35
|
# Money.new(5, "USD").allocate([0.50, 0.25, 0.25])
|
|
@@ -38,29 +47,49 @@ class Money
|
|
|
38
47
|
# Money.new(30, "USD").allocate([Rational(2, 3), Rational(1, 3)])
|
|
39
48
|
# #=> [#<Money value:20.00 currency:USD>, #<Money value:10.00 currency:USD>]
|
|
40
49
|
|
|
41
|
-
# @example left over
|
|
50
|
+
# @example left over subunits distributed reverse order when using roundrobin_reverse strategy
|
|
42
51
|
# Money.new(10.01, "USD").allocate([0.5, 0.5], :roundrobin_reverse)
|
|
43
52
|
# #=> [#<Money value:5.00 currency:USD>, #<Money value:5.01 currency:USD>]
|
|
53
|
+
|
|
54
|
+
# @examples left over subunits distributed by nearest strategy
|
|
55
|
+
# Money.new(10.55, "USD").allocate([0.25, 0.5, 0.25], :nearest)
|
|
56
|
+
# #=> [#<Money value:2.64 currency:USD>, #<Money value:5.27 currency:USD>, #<Money value:2.64 currency:USD>]
|
|
57
|
+
|
|
44
58
|
def allocate(splits, strategy = :roundrobin)
|
|
59
|
+
if splits.empty?
|
|
60
|
+
raise ArgumentError, 'at least one split must be provided'
|
|
61
|
+
end
|
|
62
|
+
|
|
45
63
|
splits.map!(&:to_r)
|
|
46
64
|
allocations = splits.inject(0, :+)
|
|
47
65
|
|
|
48
66
|
if (allocations - ONE) > Float::EPSILON
|
|
49
|
-
raise ArgumentError, "
|
|
67
|
+
raise ArgumentError, "allocations add to more than 100%"
|
|
50
68
|
end
|
|
51
69
|
|
|
52
70
|
amounts, left_over = amounts_from_splits(allocations, splits)
|
|
53
71
|
|
|
72
|
+
order = case strategy
|
|
73
|
+
when :roundrobin
|
|
74
|
+
(0...left_over).to_a
|
|
75
|
+
when :roundrobin_reverse
|
|
76
|
+
(0...amounts.length).to_a.reverse
|
|
77
|
+
when :nearest
|
|
78
|
+
rank_by_nearest(amounts)
|
|
79
|
+
else
|
|
80
|
+
raise ArgumentError, "Invalid strategy. Valid options: :roundrobin, :roundrobin_reverse, :nearest"
|
|
81
|
+
end
|
|
82
|
+
|
|
54
83
|
left_over.to_i.times do |i|
|
|
55
|
-
amounts[
|
|
84
|
+
amounts[order[i]][:whole_subunits] += 1
|
|
56
85
|
end
|
|
57
86
|
|
|
58
|
-
amounts.
|
|
87
|
+
amounts.map { |amount| Money.from_subunits(amount[:whole_subunits], currency) }
|
|
59
88
|
end
|
|
60
89
|
|
|
61
90
|
# Allocates money between different parties up to the maximum amounts specified.
|
|
62
|
-
# Left over
|
|
63
|
-
#
|
|
91
|
+
# Left over subunits will be assigned round-robin up to the maximum specified.
|
|
92
|
+
# Subunits are dropped when the maximums are attained.
|
|
64
93
|
#
|
|
65
94
|
# @example
|
|
66
95
|
# Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.75)])
|
|
@@ -90,6 +119,7 @@ class Money
|
|
|
90
119
|
total_allocatable = [maximums_total.subunits, self.subunits].min
|
|
91
120
|
|
|
92
121
|
subunits_amounts, left_over = amounts_from_splits(1, splits, total_allocatable)
|
|
122
|
+
subunits_amounts.map! { |amount| amount[:whole_subunits] }
|
|
93
123
|
|
|
94
124
|
subunits_amounts.each_with_index do |amount, index|
|
|
95
125
|
break unless left_over > 0
|
|
@@ -119,10 +149,14 @@ class Money
|
|
|
119
149
|
|
|
120
150
|
left_over = subunits_to_split
|
|
121
151
|
|
|
122
|
-
amounts = splits.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
152
|
+
amounts = splits.map do |ratio|
|
|
153
|
+
whole_subunits = (subunits_to_split * ratio / allocations.to_r).floor
|
|
154
|
+
fractional_subunits = (subunits_to_split * ratio / allocations.to_r).to_f - whole_subunits
|
|
155
|
+
left_over -= whole_subunits
|
|
156
|
+
{
|
|
157
|
+
:whole_subunits => whole_subunits,
|
|
158
|
+
:fractional_subunits => fractional_subunits
|
|
159
|
+
}
|
|
126
160
|
end
|
|
127
161
|
|
|
128
162
|
[amounts, left_over]
|
|
@@ -132,15 +166,13 @@ class Money
|
|
|
132
166
|
splits.all? { |split| split.is_a?(Rational) }
|
|
133
167
|
end
|
|
134
168
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
raise ArgumentError, "Invalid strategy. Valid options: :roundrobin, :roundrobin_reverse"
|
|
143
|
-
end
|
|
169
|
+
# Given a list of decimal numbers, return a list ordered by which is nearest to the next whole number.
|
|
170
|
+
# For instance, given inputs [1.1, 1.5, 1.9] the correct ranking is 2, 1, 0. This is because 1.9 is nearly 2.
|
|
171
|
+
# Note that we are not ranking by absolute size, we only care about the distance between our input number and
|
|
172
|
+
# the next whole number. Similarly, given the input [9.1, 5.5, 3.9] the correct ranking is *still* 2, 1, 0. This
|
|
173
|
+
# is because 3.9 is nearer to 4 than 9.1 is to 10.
|
|
174
|
+
def rank_by_nearest(amounts)
|
|
175
|
+
amounts.each_with_index.sort_by{ |amount, i| 1 - amount[:fractional_subunits] }.map(&:last)
|
|
144
176
|
end
|
|
145
177
|
end
|
|
146
178
|
end
|
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,20 @@ 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
|
+
return amount if amount.currency.compatible?(Helpers.value_to_currency(currency))
|
|
110
|
+
|
|
111
|
+
msg = "Money.new is attempting to change currency of an existing money object"
|
|
112
|
+
if Money.config.legacy_deprecations
|
|
113
|
+
Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
|
|
114
|
+
return Money.new(amount.value, currency)
|
|
115
|
+
else
|
|
116
|
+
raise Money::IncompatibleCurrencyError, msg
|
|
117
|
+
end
|
|
118
|
+
end
|
|
79
119
|
end
|
|
80
120
|
configure
|
|
81
121
|
|
|
@@ -137,13 +177,8 @@ class Money
|
|
|
137
177
|
end
|
|
138
178
|
|
|
139
179
|
def *(numeric)
|
|
140
|
-
unless numeric.is_a?(Numeric)
|
|
141
|
-
|
|
142
|
-
Money.deprecate("Multiplying Money with #{numeric.class.name} is deprecated and will be removed in the next major release.")
|
|
143
|
-
else
|
|
144
|
-
raise ArgumentError, "Money objects can only be multiplied by a Numeric"
|
|
145
|
-
end
|
|
146
|
-
end
|
|
180
|
+
raise ArgumentError, "Money objects can only be multiplied by a Numeric" unless numeric.is_a?(Numeric)
|
|
181
|
+
|
|
147
182
|
return self if numeric == 1
|
|
148
183
|
Money.new(value.to_r * numeric, currency)
|
|
149
184
|
end
|
|
@@ -167,43 +202,22 @@ class Money
|
|
|
167
202
|
value == other.value
|
|
168
203
|
end
|
|
169
204
|
|
|
170
|
-
class ReverseOperationProxy
|
|
171
|
-
include Comparable
|
|
172
|
-
|
|
173
|
-
def initialize(value)
|
|
174
|
-
@value = value
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def <=>(other)
|
|
178
|
-
-(other <=> @value)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def +(other)
|
|
182
|
-
other + @value
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def -(other)
|
|
186
|
-
-(other - @value)
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def *(other)
|
|
190
|
-
other * @value
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
205
|
def coerce(other)
|
|
195
206
|
raise TypeError, "Money can't be coerced into #{other.class}" unless other.is_a?(Numeric)
|
|
196
207
|
[ReverseOperationProxy.new(other), self]
|
|
197
208
|
end
|
|
198
209
|
|
|
199
|
-
def to_money(
|
|
200
|
-
if
|
|
201
|
-
return
|
|
210
|
+
def to_money(new_currency = nil)
|
|
211
|
+
if new_currency.nil?
|
|
212
|
+
return self
|
|
202
213
|
end
|
|
203
214
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
if no_currency?
|
|
216
|
+
return Money.new(value, new_currency)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
unless currency.compatible?(Helpers.value_to_currency(new_currency))
|
|
220
|
+
msg = "to_money is attempting to change currency of an existing money object from #{currency} to #{new_currency}"
|
|
207
221
|
if Money.config.legacy_deprecations
|
|
208
222
|
Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
|
|
209
223
|
else
|
|
@@ -298,12 +312,12 @@ class Money
|
|
|
298
312
|
#
|
|
299
313
|
# @param [2] number of parties.
|
|
300
314
|
#
|
|
301
|
-
# @return [
|
|
315
|
+
# @return [Enumerable<Money, Money, Money>]
|
|
302
316
|
#
|
|
303
317
|
# @example
|
|
304
|
-
# Money.new(100, "USD").split(3) #=> [Money.new(34), Money.new(33), Money.new(33)]
|
|
318
|
+
# Money.new(100, "USD").split(3) #=> Enumerable[Money.new(34), Money.new(33), Money.new(33)]
|
|
305
319
|
def split(num)
|
|
306
|
-
|
|
320
|
+
Splitter.new(self, num)
|
|
307
321
|
end
|
|
308
322
|
|
|
309
323
|
# Calculate the splits evenly without losing pennies.
|
|
@@ -318,17 +332,7 @@ class Money
|
|
|
318
332
|
# @example
|
|
319
333
|
# Money.new(100, "USD").calculate_splits(3) #=> {Money.new(34) => 1, Money.new(33) => 2}
|
|
320
334
|
def calculate_splits(num)
|
|
321
|
-
|
|
322
|
-
subunits = self.subunits
|
|
323
|
-
low = Money.from_subunits(subunits / num, currency)
|
|
324
|
-
high = Money.from_subunits(low.subunits + 1, currency)
|
|
325
|
-
|
|
326
|
-
num_high = subunits % num
|
|
327
|
-
|
|
328
|
-
{}.tap do |result|
|
|
329
|
-
result[high] = num_high if num_high > 0
|
|
330
|
-
result[low] = num - num_high
|
|
331
|
-
end
|
|
335
|
+
Splitter.new(self, num).split.dup
|
|
332
336
|
end
|
|
333
337
|
|
|
334
338
|
# Clamps the value to be within the specified minimum and maximum. Returns
|
|
@@ -355,10 +359,24 @@ class Money
|
|
|
355
359
|
private
|
|
356
360
|
|
|
357
361
|
def arithmetic(money_or_numeric)
|
|
358
|
-
|
|
359
|
-
|
|
362
|
+
case money_or_numeric
|
|
363
|
+
when Money
|
|
364
|
+
unless currency.compatible?(money_or_numeric.currency)
|
|
365
|
+
msg = "mathematical operation not permitted for Money objects with different currencies #{money_or_numeric.currency} and #{currency}."
|
|
366
|
+
if Money.config.legacy_deprecations
|
|
367
|
+
Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
|
|
368
|
+
else
|
|
369
|
+
raise Money::IncompatibleCurrencyError, msg
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
yield(money_or_numeric)
|
|
373
|
+
|
|
374
|
+
when Numeric
|
|
375
|
+
yield(Money.new(money_or_numeric, currency))
|
|
360
376
|
|
|
361
|
-
|
|
377
|
+
else
|
|
378
|
+
raise TypeError, "#{money_or_numeric.class.name} can't be coerced into Money"
|
|
379
|
+
end
|
|
362
380
|
end
|
|
363
381
|
|
|
364
382
|
def calculated_currency(other)
|