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
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/.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
|
|
@@ -57,6 +58,30 @@ m.allocate([Rational(2, 3), Rational(1, 3)]).map(&:value) == [666.67, 333.33]
|
|
57
58
|
m.allocate_max_amounts([500, 300, 200]).map(&:value) == [500, 300, 200]
|
58
59
|
m.allocate_max_amounts([500, 300, 300]).map(&:value) == [454.55, 272.73, 272.72]
|
59
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
|
+
|
60
85
|
# Clamp
|
61
86
|
Money.new(50, "USD").clamp(1, 100) == Money.new(50, "USD")
|
62
87
|
|
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/helpers.rb
CHANGED
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
|
|
@@ -162,43 +202,22 @@ class Money
|
|
162
202
|
value == other.value
|
163
203
|
end
|
164
204
|
|
165
|
-
class ReverseOperationProxy
|
166
|
-
include Comparable
|
167
|
-
|
168
|
-
def initialize(value)
|
169
|
-
@value = value
|
170
|
-
end
|
171
|
-
|
172
|
-
def <=>(other)
|
173
|
-
-(other <=> @value)
|
174
|
-
end
|
175
|
-
|
176
|
-
def +(other)
|
177
|
-
other + @value
|
178
|
-
end
|
179
|
-
|
180
|
-
def -(other)
|
181
|
-
-(other - @value)
|
182
|
-
end
|
183
|
-
|
184
|
-
def *(other)
|
185
|
-
other * @value
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
205
|
def coerce(other)
|
190
206
|
raise TypeError, "Money can't be coerced into #{other.class}" unless other.is_a?(Numeric)
|
191
207
|
[ReverseOperationProxy.new(other), self]
|
192
208
|
end
|
193
209
|
|
194
|
-
def to_money(
|
195
|
-
if
|
196
|
-
return
|
210
|
+
def to_money(new_currency = nil)
|
211
|
+
if new_currency.nil?
|
212
|
+
return self
|
213
|
+
end
|
214
|
+
|
215
|
+
if no_currency?
|
216
|
+
return Money.new(value, new_currency)
|
197
217
|
end
|
198
218
|
|
199
|
-
|
200
|
-
|
201
|
-
msg = "mathematical operation not permitted for Money objects with different currencies #{curr} and #{currency}"
|
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}"
|
202
221
|
if Money.config.legacy_deprecations
|
203
222
|
Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
|
204
223
|
else
|
@@ -293,12 +312,12 @@ class Money
|
|
293
312
|
#
|
294
313
|
# @param [2] number of parties.
|
295
314
|
#
|
296
|
-
# @return [
|
315
|
+
# @return [Enumerable<Money, Money, Money>]
|
297
316
|
#
|
298
317
|
# @example
|
299
|
-
# 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)]
|
300
319
|
def split(num)
|
301
|
-
|
320
|
+
Splitter.new(self, num)
|
302
321
|
end
|
303
322
|
|
304
323
|
# Calculate the splits evenly without losing pennies.
|
@@ -313,17 +332,7 @@ class Money
|
|
313
332
|
# @example
|
314
333
|
# Money.new(100, "USD").calculate_splits(3) #=> {Money.new(34) => 1, Money.new(33) => 2}
|
315
334
|
def calculate_splits(num)
|
316
|
-
|
317
|
-
subunits = self.subunits
|
318
|
-
low = Money.from_subunits(subunits / num, currency)
|
319
|
-
high = Money.from_subunits(low.subunits + 1, currency)
|
320
|
-
|
321
|
-
num_high = subunits % num
|
322
|
-
|
323
|
-
{}.tap do |result|
|
324
|
-
result[high] = num_high if num_high > 0
|
325
|
-
result[low] = num - num_high
|
326
|
-
end
|
335
|
+
Splitter.new(self, num).split.dup
|
327
336
|
end
|
328
337
|
|
329
338
|
# Clamps the value to be within the specified minimum and maximum. Returns
|
@@ -350,10 +359,24 @@ class Money
|
|
350
359
|
private
|
351
360
|
|
352
361
|
def arithmetic(money_or_numeric)
|
353
|
-
|
354
|
-
|
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))
|
355
376
|
|
356
|
-
|
377
|
+
else
|
378
|
+
raise TypeError, "#{money_or_numeric.class.name} can't be coerced into Money"
|
379
|
+
end
|
357
380
|
end
|
358
381
|
|
359
382
|
def calculated_currency(other)
|