shopify-money 1.3.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|