shopify-money 0.10.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 +7 -0
- data/.document +5 -0
- data/.gitignore +51 -0
- data/.rspec +1 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +20 -0
- data/README.md +156 -0
- data/Rakefile +42 -0
- data/bin/console +14 -0
- data/circle.yml +13 -0
- data/config/currency_historic.json +157 -0
- data/config/currency_iso.json +2642 -0
- data/config/currency_non_iso.json +82 -0
- data/dev.yml +9 -0
- data/lib/money.rb +10 -0
- data/lib/money/accounting_money_parser.rb +8 -0
- data/lib/money/core_extensions.rb +18 -0
- data/lib/money/currency.rb +59 -0
- data/lib/money/currency/loader.rb +26 -0
- data/lib/money/deprecations.rb +18 -0
- data/lib/money/helpers.rb +71 -0
- data/lib/money/money.rb +408 -0
- data/lib/money/money_parser.rb +152 -0
- data/lib/money/null_currency.rb +35 -0
- data/lib/money/version.rb +3 -0
- data/lib/money_accessor.rb +32 -0
- data/lib/money_column.rb +3 -0
- data/lib/money_column/active_record_hooks.rb +95 -0
- data/lib/money_column/active_record_type.rb +6 -0
- data/lib/money_column/railtie.rb +7 -0
- data/money.gemspec +27 -0
- data/spec/accounting_money_parser_spec.rb +204 -0
- data/spec/core_extensions_spec.rb +44 -0
- data/spec/currency/loader_spec.rb +21 -0
- data/spec/currency_spec.rb +113 -0
- data/spec/helpers_spec.rb +103 -0
- data/spec/money_accessor_spec.rb +86 -0
- data/spec/money_column_spec.rb +298 -0
- data/spec/money_parser_spec.rb +355 -0
- data/spec/money_spec.rb +853 -0
- data/spec/null_currency_spec.rb +46 -0
- data/spec/schema.rb +9 -0
- data/spec/spec_helper.rb +74 -0
- metadata +196 -0
@@ -0,0 +1,355 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe MoneyParser do
|
4
|
+
before(:each) do
|
5
|
+
@parser = MoneyParser
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "parsing of amounts with period decimal separator" do
|
9
|
+
it "parses an empty string to $0" do
|
10
|
+
expect(@parser.parse("")).to eq(Money.zero)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "parses an invalid string when not strict" do
|
14
|
+
expect(Money).to receive(:deprecate).twice
|
15
|
+
expect(@parser.parse("no money")).to eq(Money.zero)
|
16
|
+
expect(@parser.parse("1..")).to eq(Money.new(1))
|
17
|
+
end
|
18
|
+
|
19
|
+
it "parses raise with an invalid string and strict option" do
|
20
|
+
expect { @parser.parse("no money", strict: true) }.to raise_error(MoneyParser::MoneyFormatError)
|
21
|
+
expect { @parser.parse("1..1", strict: true) }.to raise_error(MoneyParser::MoneyFormatError)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "parses a single digit integer string" do
|
25
|
+
expect(@parser.parse("1")).to eq(Money.new(1.00))
|
26
|
+
end
|
27
|
+
|
28
|
+
it "parses a double digit integer string" do
|
29
|
+
expect(@parser.parse("10")).to eq(Money.new(10.00))
|
30
|
+
end
|
31
|
+
|
32
|
+
it "parses an integer string amount with a leading $" do
|
33
|
+
expect(@parser.parse("$1")).to eq(Money.new(1.00))
|
34
|
+
end
|
35
|
+
|
36
|
+
it "parses a float string amount" do
|
37
|
+
expect(@parser.parse("1.37")).to eq(Money.new(1.37))
|
38
|
+
end
|
39
|
+
|
40
|
+
it "parses a float string amount with a leading $" do
|
41
|
+
expect(@parser.parse("$1.37")).to eq(Money.new(1.37))
|
42
|
+
end
|
43
|
+
|
44
|
+
it "parses a float string with a single digit after the decimal" do
|
45
|
+
expect(@parser.parse("10.0")).to eq(Money.new(10.00))
|
46
|
+
end
|
47
|
+
|
48
|
+
it "parses a float string with two digits after the decimal" do
|
49
|
+
expect(@parser.parse("10.00")).to eq(Money.new(10.00))
|
50
|
+
end
|
51
|
+
|
52
|
+
it "parses the amount from an amount surrounded by whitespace and garbage" do
|
53
|
+
expect(@parser.parse("Rubbish $1.00 Rubbish")).to eq(Money.new(1.00))
|
54
|
+
end
|
55
|
+
|
56
|
+
it "parses the amount from an amount surrounded by garbage" do
|
57
|
+
expect(@parser.parse("Rubbish$1.00Rubbish")).to eq(Money.new(1.00))
|
58
|
+
end
|
59
|
+
|
60
|
+
it "parses a negative integer amount in the hundreds" do
|
61
|
+
expect(@parser.parse("-100")).to eq(Money.new(-100.00))
|
62
|
+
end
|
63
|
+
|
64
|
+
it "parses an integer amount in the hundreds" do
|
65
|
+
expect(@parser.parse("410")).to eq(Money.new(410.00))
|
66
|
+
end
|
67
|
+
|
68
|
+
it "parses an amount ending with a ." do
|
69
|
+
expect(@parser.parse("1.")).to eq(Money.new(1))
|
70
|
+
expect(@parser.parse("100,000.")).to eq(Money.new(100_000))
|
71
|
+
end
|
72
|
+
|
73
|
+
it "parses an amount starting with a ." do
|
74
|
+
expect(@parser.parse(".12")).to eq(Money.new(0.12))
|
75
|
+
end
|
76
|
+
|
77
|
+
it "parses a positive amount with a thousands separator" do
|
78
|
+
expect(@parser.parse("100,000.00")).to eq(Money.new(100_000.00))
|
79
|
+
end
|
80
|
+
|
81
|
+
it "parses a negative amount with a thousands separator" do
|
82
|
+
expect(@parser.parse("-100,000.00")).to eq(Money.new(-100_000.00))
|
83
|
+
end
|
84
|
+
|
85
|
+
it "parses a positive amount with a thousands separator with no decimal" do
|
86
|
+
expect(@parser.parse("1,000")).to eq(Money.new(1_000))
|
87
|
+
end
|
88
|
+
|
89
|
+
it "parses a positive amount with a thousands separator with no decimal with a currency" do
|
90
|
+
expect(@parser.parse("1,000", 'JOD')).to eq(Money.new(1_000, 'JOD'))
|
91
|
+
end
|
92
|
+
|
93
|
+
it "parses a positive amount with a thousands separator with no decimal" do
|
94
|
+
expect(@parser.parse("12,34,567.89", 'INR')).to eq(Money.new(1_234_567.89, 'INR'))
|
95
|
+
end
|
96
|
+
|
97
|
+
it "parses negative $1.00" do
|
98
|
+
expect(@parser.parse("-1.00")).to eq(Money.new(-1.00))
|
99
|
+
end
|
100
|
+
|
101
|
+
it "parses a negative cents amount" do
|
102
|
+
expect(@parser.parse("-0.90")).to eq(Money.new(-0.90))
|
103
|
+
end
|
104
|
+
|
105
|
+
it "parses amount with 3 decimals and 0 dollar amount" do
|
106
|
+
expect(@parser.parse("0.123")).to eq(Money.new(0.12))
|
107
|
+
end
|
108
|
+
|
109
|
+
it "parses negative amount with 3 decimals and 0 dollar amount" do
|
110
|
+
expect(@parser.parse("-0.123")).to eq(Money.new(-0.12))
|
111
|
+
end
|
112
|
+
|
113
|
+
it "parses negative amount with multiple leading - signs" do
|
114
|
+
expect(@parser.parse("--0.123")).to eq(Money.new(-0.12))
|
115
|
+
end
|
116
|
+
|
117
|
+
it "parses negative amount with multiple - signs" do
|
118
|
+
expect(@parser.parse("--0.123--")).to eq(Money.new(-0.12))
|
119
|
+
end
|
120
|
+
|
121
|
+
it "parses a positive amount with a thousands dot separator currency and no decimal" do
|
122
|
+
expect(@parser.parse("1.000", 'EUR')).to eq(Money.new(1_000, 'EUR'))
|
123
|
+
end
|
124
|
+
|
125
|
+
it "parses a three digit currency" do
|
126
|
+
expect(@parser.parse("1.000", 'JOD')).to eq(Money.new(1, 'JOD'))
|
127
|
+
end
|
128
|
+
|
129
|
+
it "parses uses currency when passed as block to with_currency" do
|
130
|
+
expect(Money.with_currency('JOD') { @parser.parse("1.000") }).to eq(Money.new(1, 'JOD'))
|
131
|
+
end
|
132
|
+
|
133
|
+
it "parses no currency amount" do
|
134
|
+
expect(@parser.parse("1.000", Money::NULL_CURRENCY)).to eq(Money.new(1000, Money::NULL_CURRENCY))
|
135
|
+
end
|
136
|
+
|
137
|
+
it "parses amount with more than 3 decimals correctly" do
|
138
|
+
expect(@parser.parse("1.11111111")).to eq(Money.new(1.11))
|
139
|
+
end
|
140
|
+
|
141
|
+
it "parses amount with multiple consistent thousands delimiters" do
|
142
|
+
expect(@parser.parse("1.111.111")).to eq(Money.new(1_111_111))
|
143
|
+
end
|
144
|
+
|
145
|
+
it "parses amount with multiple inconsistent thousands delimiters" do
|
146
|
+
expect(Money).to receive(:deprecate).once
|
147
|
+
expect(@parser.parse("1.1.11.111")).to eq(Money.new(1_111_111))
|
148
|
+
end
|
149
|
+
|
150
|
+
it "parses raises with multiple inconsistent thousands delimiters and strict option" do
|
151
|
+
expect { @parser.parse("1.1.11.111", strict: true) }.to raise_error(MoneyParser::MoneyFormatError)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
describe "parsing of amounts with comma decimal separator" do
|
156
|
+
it "parses dollar amount $1,00 with leading $" do
|
157
|
+
expect(@parser.parse("$1,00")).to eq(Money.new(1.00))
|
158
|
+
end
|
159
|
+
|
160
|
+
it "parses dollar amount $1,37 with leading $, and non-zero cents" do
|
161
|
+
expect(@parser.parse("$1,37")).to eq(Money.new(1.37))
|
162
|
+
end
|
163
|
+
|
164
|
+
it "parses the amount from an amount surrounded by whitespace and garbage" do
|
165
|
+
expect(@parser.parse("Rubbish $1,00 Rubbish")).to eq(Money.new(1.00))
|
166
|
+
end
|
167
|
+
|
168
|
+
it "parses the amount from an amount surrounded by garbage" do
|
169
|
+
expect(@parser.parse("Rubbish$1,00Rubbish")).to eq(Money.new(1.00))
|
170
|
+
end
|
171
|
+
|
172
|
+
it "parses negative hundreds amount" do
|
173
|
+
expect(@parser.parse("-100,00")).to eq(Money.new(-100.00))
|
174
|
+
end
|
175
|
+
|
176
|
+
it "parses positive hundreds amount" do
|
177
|
+
expect(@parser.parse("410,00")).to eq(Money.new(410.00))
|
178
|
+
end
|
179
|
+
|
180
|
+
it "parses a positive amount with a thousands separator" do
|
181
|
+
expect(@parser.parse("100.000,00")).to eq(Money.new(100_000.00))
|
182
|
+
end
|
183
|
+
|
184
|
+
it "parses a negative amount with a thousands separator" do
|
185
|
+
expect(@parser.parse("-100.000,00")).to eq(Money.new(-100_000.00))
|
186
|
+
end
|
187
|
+
|
188
|
+
it "parses amount ending with a comma" do
|
189
|
+
expect(@parser.parse("1,")).to eq(Money.new(1))
|
190
|
+
expect(@parser.parse("100.000,")).to eq(Money.new(100_000))
|
191
|
+
end
|
192
|
+
|
193
|
+
it "parses amount with 3 decimals and 0 dollar amount" do
|
194
|
+
expect(@parser.parse("0,123")).to eq(Money.new(0.12))
|
195
|
+
end
|
196
|
+
|
197
|
+
it "parses negative amount with 3 decimals and 0 dollar amount" do
|
198
|
+
expect(@parser.parse("-0,123")).to eq(Money.new(-0.12))
|
199
|
+
end
|
200
|
+
|
201
|
+
it "parses amount 2 decimals correctly" do
|
202
|
+
expect(@parser.parse("1,11", Money::NULL_CURRENCY)).to eq(Money.new(1.11, Money::NULL_CURRENCY))
|
203
|
+
end
|
204
|
+
|
205
|
+
it "parses amount with more than 3 decimals correctly" do
|
206
|
+
expect(@parser.parse("1,11111111", Money::NULL_CURRENCY)).to eq(Money.new(111_111_111, Money::NULL_CURRENCY))
|
207
|
+
end
|
208
|
+
|
209
|
+
it "parses amount with more than 3 decimals correctly and a currency" do
|
210
|
+
expect(@parser.parse("1,11111111", 'CAD')).to eq(Money.new(111_111_111))
|
211
|
+
end
|
212
|
+
|
213
|
+
it "parses amount with multiple consistent thousands delimiters" do
|
214
|
+
expect(@parser.parse("1,111,111")).to eq(Money.new(1_111_111))
|
215
|
+
end
|
216
|
+
|
217
|
+
it "parses amount with multiple inconsistent thousands delimiters" do
|
218
|
+
expect(Money).to receive(:deprecate).once
|
219
|
+
expect(@parser.parse("1,1,11,111")).to eq(Money.new(1_111_111))
|
220
|
+
end
|
221
|
+
|
222
|
+
it "parses raises with multiple inconsistent thousands delimiters and strict option" do
|
223
|
+
expect { @parser.parse("1,1,11,111", strict: true) }.to raise_error(MoneyParser::MoneyFormatError)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
describe "3 digit decimal currency" do
|
228
|
+
it "parses thousands correctly" do
|
229
|
+
expect(@parser.parse("1,111", "JOD")).to eq(Money.new(1_111, 'JOD'))
|
230
|
+
expect(@parser.parse("1.111.111", "JOD")).to eq(Money.new(1_111_111, 'JOD'))
|
231
|
+
expect(@parser.parse("1 111", "JOD")).to eq(Money.new(1_111, 'JOD'))
|
232
|
+
expect(@parser.parse("1111,111", "JOD")).to eq(Money.new(1_111_111, 'JOD'))
|
233
|
+
end
|
234
|
+
|
235
|
+
it "parses decimals correctly" do
|
236
|
+
expect(@parser.parse("1.111", "JOD")).to eq(Money.new(1.111, 'JOD'))
|
237
|
+
expect(@parser.parse("1,11", "JOD")).to eq(Money.new(1.110, 'JOD'))
|
238
|
+
expect(@parser.parse("1111.111", "JOD")).to eq(Money.new(1_111.111, 'JOD'))
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
describe "no decimal currency" do
|
243
|
+
it "parses thousands correctly" do
|
244
|
+
expect(@parser.parse("1,111", "JPY")).to eq(Money.new(1_111, 'JPY'))
|
245
|
+
expect(@parser.parse("1.111", "JPY")).to eq(Money.new(1_111, 'JPY'))
|
246
|
+
expect(@parser.parse("1 111", "JPY")).to eq(Money.new(1_111, 'JPY'))
|
247
|
+
expect(@parser.parse("1111,111", "JPY")).to eq(Money.new(1_111_111, 'JPY'))
|
248
|
+
end
|
249
|
+
|
250
|
+
it "parses decimals correctly" do
|
251
|
+
expect(@parser.parse("1,11", "JPY")).to eq(Money.new(1, 'JPY'))
|
252
|
+
expect(@parser.parse("1.11", "JPY")).to eq(Money.new(1, 'JPY'))
|
253
|
+
expect(@parser.parse("1111.111", "JPY")).to eq(Money.new(1111, 'JPY'))
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
describe "two decimal currency" do
|
258
|
+
it "parses thousands correctly" do
|
259
|
+
expect(@parser.parse("1,111", "USD")).to eq(Money.new(1_111, 'USD'))
|
260
|
+
expect(@parser.parse("1.111", "USD")).to eq(Money.new(1_111, 'USD'))
|
261
|
+
expect(@parser.parse("1 111", "USD")).to eq(Money.new(1_111, 'USD'))
|
262
|
+
expect(@parser.parse("1111,111", "USD")).to eq(Money.new(1_111_111, 'USD'))
|
263
|
+
end
|
264
|
+
|
265
|
+
it "parses decimals correctly" do
|
266
|
+
expect(@parser.parse("1,11", "USD")).to eq(Money.new(1.11, 'USD'))
|
267
|
+
expect(@parser.parse("1.11", "USD")).to eq(Money.new(1.11, 'USD'))
|
268
|
+
expect(@parser.parse("1111.111", "USD")).to eq(Money.new(1111.11, 'USD'))
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
describe "parsing of decimal cents amounts from 0 to 10" do
|
273
|
+
it "parses 50.0" do
|
274
|
+
expect(@parser.parse("50.0")).to eq(Money.new(50.00))
|
275
|
+
end
|
276
|
+
|
277
|
+
it "parses 50.1" do
|
278
|
+
expect(@parser.parse("50.1")).to eq(Money.new(50.10))
|
279
|
+
end
|
280
|
+
|
281
|
+
it "parses 50.2" do
|
282
|
+
expect(@parser.parse("50.2")).to eq(Money.new(50.20))
|
283
|
+
end
|
284
|
+
|
285
|
+
it "parses 50.3" do
|
286
|
+
expect(@parser.parse("50.3")).to eq(Money.new(50.30))
|
287
|
+
end
|
288
|
+
|
289
|
+
it "parses 50.4" do
|
290
|
+
expect(@parser.parse("50.4")).to eq(Money.new(50.40))
|
291
|
+
end
|
292
|
+
|
293
|
+
it "parses 50.5" do
|
294
|
+
expect(@parser.parse("50.5")).to eq(Money.new(50.50))
|
295
|
+
end
|
296
|
+
|
297
|
+
it "parses 50.6" do
|
298
|
+
expect(@parser.parse("50.6")).to eq(Money.new(50.60))
|
299
|
+
end
|
300
|
+
|
301
|
+
it "parses 50.7" do
|
302
|
+
expect(@parser.parse("50.7")).to eq(Money.new(50.70))
|
303
|
+
end
|
304
|
+
|
305
|
+
it "parses 50.8" do
|
306
|
+
expect(@parser.parse("50.8")).to eq(Money.new(50.80))
|
307
|
+
end
|
308
|
+
|
309
|
+
it "parses 50.9" do
|
310
|
+
expect(@parser.parse("50.9")).to eq(Money.new(50.90))
|
311
|
+
end
|
312
|
+
|
313
|
+
it "parses 50.10" do
|
314
|
+
expect(@parser.parse("50.10")).to eq(Money.new(50.10))
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
describe "parsing of fixnum" do
|
319
|
+
it "parses 1" do
|
320
|
+
expect(@parser.parse(1)).to eq(Money.new(1))
|
321
|
+
end
|
322
|
+
|
323
|
+
it "parses 50" do
|
324
|
+
expect(@parser.parse(50)).to eq(Money.new(50))
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
describe "parsing of float" do
|
329
|
+
it "parses 1.00" do
|
330
|
+
expect(@parser.parse(1.00)).to eq(Money.new(1.00))
|
331
|
+
end
|
332
|
+
|
333
|
+
it "parses 1.32" do
|
334
|
+
expect(@parser.parse(1.32)).to eq(Money.new(1.32))
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
describe "parsing with thousands separators" do
|
339
|
+
[
|
340
|
+
'1,234,567.89',
|
341
|
+
'1 234 567.89',
|
342
|
+
'1 234 567,89',
|
343
|
+
'1.234.567,89',
|
344
|
+
'1˙234˙567,89',
|
345
|
+
'12,34,567.89',
|
346
|
+
"1'234'567.89",
|
347
|
+
"1'234'567,89",
|
348
|
+
'123,4567.89',
|
349
|
+
].each do |number|
|
350
|
+
it "parses #{number}" do
|
351
|
+
expect(@parser.parse(number)).to eq(Money.new(1_234_567.89))
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
data/spec/money_spec.rb
ADDED
@@ -0,0 +1,853 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
RSpec.describe "Money" do
|
5
|
+
|
6
|
+
let (:money) {Money.new(1)}
|
7
|
+
let (:amount_money) { Money.new(1.23, 'USD') }
|
8
|
+
let (:non_fractional_money) { Money.new(1, 'JPY') }
|
9
|
+
let (:zero_money) { Money.new(0) }
|
10
|
+
|
11
|
+
it "is contructable with empty class method" do
|
12
|
+
expect(Money.empty).to eq(Money.new)
|
13
|
+
end
|
14
|
+
|
15
|
+
context "default currency not set" do
|
16
|
+
before(:each) do
|
17
|
+
@default_currency = Money.default_currency
|
18
|
+
Money.default_currency = nil
|
19
|
+
end
|
20
|
+
after(:each) do
|
21
|
+
Money.default_currency = @default_currency
|
22
|
+
end
|
23
|
+
|
24
|
+
it "raises an error" do
|
25
|
+
expect { money }.to raise_error(ArgumentError)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it ".zero has no currency" do
|
30
|
+
expect(Money.zero.currency).to be_a(Money::NullCurrency)
|
31
|
+
end
|
32
|
+
|
33
|
+
it ".zero is a 0$ value" do
|
34
|
+
expect(Money.zero).to eq(Money.new(0))
|
35
|
+
end
|
36
|
+
|
37
|
+
it "returns itself with to_money" do
|
38
|
+
expect(money.to_money).to eq(money)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "defaults to 0 when constructed with no arguments" do
|
42
|
+
expect(Money.new).to eq(Money.new(0))
|
43
|
+
end
|
44
|
+
|
45
|
+
it "defaults to 0 when constructed with an empty string" do
|
46
|
+
expect(Money.new('')).to eq(Money.new(0))
|
47
|
+
end
|
48
|
+
|
49
|
+
it "defaults to 0 when constructed with an invalid string" do
|
50
|
+
expect(Money).to receive(:deprecate).once
|
51
|
+
expect(Money.new('invalid')).to eq(Money.new(0.00))
|
52
|
+
end
|
53
|
+
|
54
|
+
it "to_s correctly displays the right number of decimal places" do
|
55
|
+
expect(money.to_s).to eq("1.00")
|
56
|
+
expect(non_fractional_money.to_s).to eq("1")
|
57
|
+
end
|
58
|
+
|
59
|
+
it "to_s with a legacy_dollars style" do
|
60
|
+
expect(amount_money.to_s(:legacy_dollars)).to eq("1.23")
|
61
|
+
expect(non_fractional_money.to_s(:legacy_dollars)).to eq("1.00")
|
62
|
+
end
|
63
|
+
|
64
|
+
it "to_s with a amount style" do
|
65
|
+
expect(amount_money.to_s(:amount)).to eq("1.23")
|
66
|
+
expect(non_fractional_money.to_s(:amount)).to eq("1")
|
67
|
+
end
|
68
|
+
|
69
|
+
it "as_json as a float with 2 decimal places" do
|
70
|
+
expect(money.as_json).to eq("1.00")
|
71
|
+
end
|
72
|
+
|
73
|
+
it "is constructable with a BigDecimal" do
|
74
|
+
expect(Money.new(BigDecimal.new("1.23"))).to eq(Money.new(1.23))
|
75
|
+
end
|
76
|
+
|
77
|
+
it "is constructable with a Fixnum" do
|
78
|
+
expect(Money.new(3)).to eq(Money.new(3.00))
|
79
|
+
end
|
80
|
+
|
81
|
+
it "is construcatable with a Float" do
|
82
|
+
expect(Money.new(3.00)).to eq(Money.new(BigDecimal.new('3.00')))
|
83
|
+
end
|
84
|
+
|
85
|
+
it "is construcatable with a String" do
|
86
|
+
expect(Money.new('3.00')).to eq(Money.new(3.00))
|
87
|
+
end
|
88
|
+
|
89
|
+
it "is aware of the currency" do
|
90
|
+
expect(Money.new(1.00, 'CAD').currency.iso_code).to eq('CAD')
|
91
|
+
end
|
92
|
+
|
93
|
+
it "is addable" do
|
94
|
+
expect((Money.new(1.51) + Money.new(3.49))).to eq(Money.new(5.00))
|
95
|
+
end
|
96
|
+
|
97
|
+
it "keeps currency across calculations" do
|
98
|
+
expect(Money.new(1, 'USD') - Money.new(1, 'USD') + Money.new(1.23, Money::NULL_CURRENCY)).to eq(Money.new(1.23, 'USD'))
|
99
|
+
end
|
100
|
+
|
101
|
+
it "raises error if added other is not compatible" do
|
102
|
+
expect{ Money.new(5.00) + nil }.to raise_error(TypeError)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "is able to add $0 + $0" do
|
106
|
+
expect((Money.new + Money.new)).to eq(Money.new)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "adds inconsistent currencies" do
|
110
|
+
expect(Money).to receive(:deprecate).once
|
111
|
+
expect(Money.new(5, 'USD') + Money.new(1, 'CAD')).to eq(Money.new(6, 'USD'))
|
112
|
+
end
|
113
|
+
|
114
|
+
it "is subtractable" do
|
115
|
+
expect((Money.new(5.00) - Money.new(3.49))).to eq(Money.new(1.51))
|
116
|
+
end
|
117
|
+
|
118
|
+
it "raises error if subtracted other is not compatible" do
|
119
|
+
expect{ Money.new(5.00) - nil }.to raise_error(TypeError)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "is subtractable to $0" do
|
123
|
+
expect((Money.new(5.00) - Money.new(5.00))).to eq(Money.new)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "logs a deprecation warning when adding across currencies" do
|
127
|
+
expect(Money).to receive(:deprecate)
|
128
|
+
expect(Money.new(10, 'USD') - Money.new(1, 'JPY')).to eq(Money.new(9, 'USD'))
|
129
|
+
end
|
130
|
+
|
131
|
+
it "keeps currency when doing a computation with a null currency" do
|
132
|
+
currency = Money.new(10, 'JPY')
|
133
|
+
no_currency = Money.new(1, Money::NULL_CURRENCY)
|
134
|
+
expect((no_currency + currency).currency).to eq(Money::Currency.find!('JPY'))
|
135
|
+
expect((currency - no_currency).currency).to eq(Money::Currency.find!('JPY'))
|
136
|
+
end
|
137
|
+
|
138
|
+
it "does not log a deprecation warning when adding with a null currency value" do
|
139
|
+
currency = Money.new(10, 'USD')
|
140
|
+
no_currency = Money.new(1, Money::NULL_CURRENCY)
|
141
|
+
expect(Money).not_to receive(:deprecate)
|
142
|
+
expect(no_currency + currency).to eq(Money.new(11, 'USD'))
|
143
|
+
expect(currency - no_currency).to eq(Money.new(9, 'USD'))
|
144
|
+
end
|
145
|
+
|
146
|
+
it "is substractable to a negative amount" do
|
147
|
+
expect((Money.new(0.00) - Money.new(1.00))).to eq(Money.new("-1.00"))
|
148
|
+
end
|
149
|
+
|
150
|
+
it "is never negative zero" do
|
151
|
+
expect(Money.new(-0.00).to_s).to eq("0.00")
|
152
|
+
expect((Money.new(0) * -1).to_s).to eq("0.00")
|
153
|
+
end
|
154
|
+
|
155
|
+
it "#inspects to a presentable string" do
|
156
|
+
expect(money.inspect).to eq("#<Money value:1.00 currency:CAD>")
|
157
|
+
expect(Money.new(1, 'JPY').inspect).to eq("#<Money value:1 currency:JPY>")
|
158
|
+
expect(Money.new(1, 'JOD').inspect).to eq("#<Money value:1.000 currency:JOD>")
|
159
|
+
end
|
160
|
+
|
161
|
+
it "is inspectable within an array" do
|
162
|
+
expect([money].inspect).to eq("[#<Money value:1.00 currency:CAD>]")
|
163
|
+
end
|
164
|
+
|
165
|
+
it "correctly support eql? as a value object" do
|
166
|
+
expect(money).to eq(Money.new(1))
|
167
|
+
expect(money).to eq(Money.new(1, 'CAD'))
|
168
|
+
end
|
169
|
+
|
170
|
+
it "does not eql? with a non money object" do
|
171
|
+
expect(money).to_not eq(1)
|
172
|
+
expect(money).to_not eq(OpenStruct.new(value: 1))
|
173
|
+
end
|
174
|
+
|
175
|
+
it "does not eql? when currency missmatch" do
|
176
|
+
expect(money).to_not eq(Money.new(1, 'JPY'))
|
177
|
+
end
|
178
|
+
|
179
|
+
it "is addable with integer" do
|
180
|
+
expect((Money.new(1.33) + 1)).to eq(Money.new(2.33))
|
181
|
+
expect((1 + Money.new(1.33))).to eq(Money.new(2.33))
|
182
|
+
end
|
183
|
+
|
184
|
+
it "is addable with float" do
|
185
|
+
expect((Money.new(1.33) + 1.50)).to eq(Money.new(2.83))
|
186
|
+
expect((1.50 + Money.new(1.33))).to eq(Money.new(2.83))
|
187
|
+
end
|
188
|
+
|
189
|
+
it "is subtractable with integer" do
|
190
|
+
expect((Money.new(1.66) - 1)).to eq(Money.new(0.66))
|
191
|
+
expect((2 - Money.new(1.66))).to eq(Money.new(0.34))
|
192
|
+
end
|
193
|
+
|
194
|
+
it "is subtractable with float" do
|
195
|
+
expect((Money.new(1.66) - 1.50)).to eq(Money.new(0.16))
|
196
|
+
expect((1.50 - Money.new(1.33))).to eq(Money.new(0.17))
|
197
|
+
end
|
198
|
+
|
199
|
+
it "is multipliable with an integer" do
|
200
|
+
expect((Money.new(1.00) * 55)).to eq(Money.new(55.00))
|
201
|
+
expect((55 * Money.new(1.00))).to eq(Money.new(55.00))
|
202
|
+
end
|
203
|
+
|
204
|
+
it "is multiplable with a float" do
|
205
|
+
expect((Money.new(1.00) * 1.50)).to eq(Money.new(1.50))
|
206
|
+
expect((1.50 * Money.new(1.00))).to eq(Money.new(1.50))
|
207
|
+
end
|
208
|
+
|
209
|
+
it "is multipliable by a cents amount" do
|
210
|
+
expect((Money.new(1.00) * 0.50)).to eq(Money.new(0.50))
|
211
|
+
expect((0.50 * Money.new(1.00))).to eq(Money.new(0.50))
|
212
|
+
end
|
213
|
+
|
214
|
+
it "is multipliable by a rational" do
|
215
|
+
expect((Money.new(3.3) * Rational(1, 12))).to eq(Money.new(0.28))
|
216
|
+
expect((Rational(1, 12) * Money.new(3.3))).to eq(Money.new(0.28))
|
217
|
+
end
|
218
|
+
|
219
|
+
it "is multipliable by a repeatable floating point number" do
|
220
|
+
expect((Money.new(24.00) * (1 / 30.0))).to eq(Money.new(0.80))
|
221
|
+
expect(((1 / 30.0) * Money.new(24.00))).to eq(Money.new(0.80))
|
222
|
+
end
|
223
|
+
|
224
|
+
it "is multipliable by a repeatable floating point number where the floating point error rounds down" do
|
225
|
+
expect((Money.new(3.3) * (1.0 / 12))).to eq(Money.new(0.28))
|
226
|
+
expect(((1.0 / 12) * Money.new(3.3))).to eq(Money.new(0.28))
|
227
|
+
end
|
228
|
+
|
229
|
+
it "is multipliable by a money object" do
|
230
|
+
expect(Money).to receive(:deprecate).once
|
231
|
+
expect((Money.new(3.3) * Money.new(1))).to eq(Money.new(3.3))
|
232
|
+
end
|
233
|
+
|
234
|
+
it "rounds multiplication result with fractional penny of 5 or higher up" do
|
235
|
+
expect((Money.new(0.03) * 0.5)).to eq(Money.new(0.02))
|
236
|
+
expect((0.5 * Money.new(0.03))).to eq(Money.new(0.02))
|
237
|
+
end
|
238
|
+
|
239
|
+
it "rounds multiplication result with fractional penny of 4 or lower down" do
|
240
|
+
expect((Money.new(0.10) * 0.33)).to eq(Money.new(0.03))
|
241
|
+
expect((0.33 * Money.new(0.10))).to eq(Money.new(0.03))
|
242
|
+
end
|
243
|
+
|
244
|
+
it "is less than a bigger integer" do
|
245
|
+
expect(Money.new(1)).to be < 2
|
246
|
+
expect(2).to be > Money.new(1)
|
247
|
+
end
|
248
|
+
|
249
|
+
it "is less than or equal to a bigger integer" do
|
250
|
+
expect(Money.new(1)).to be <= 2
|
251
|
+
expect(2).to be >= Money.new(1)
|
252
|
+
end
|
253
|
+
|
254
|
+
it "is greater than a lesser integer" do
|
255
|
+
expect(Money.new(2)).to be > 1
|
256
|
+
expect(1).to be < Money.new(2)
|
257
|
+
end
|
258
|
+
|
259
|
+
it "is greater than or equal to a lesser integer" do
|
260
|
+
expect(Money.new(2)).to be >= 1
|
261
|
+
expect(1).to be <= Money.new(2)
|
262
|
+
end
|
263
|
+
|
264
|
+
it "raises if divided" do
|
265
|
+
expect { Money.new(55.00) / 55 }.to raise_error(RuntimeError)
|
266
|
+
end
|
267
|
+
|
268
|
+
it "returns cents in to_liquid" do
|
269
|
+
expect(Money.new(1.00).to_liquid).to eq(100)
|
270
|
+
end
|
271
|
+
|
272
|
+
it "returns cents in to_json" do
|
273
|
+
expect(Money.new(1.00).to_json).to eq("1.00")
|
274
|
+
end
|
275
|
+
|
276
|
+
it "supports absolute value" do
|
277
|
+
expect(Money.new(-1.00).abs).to eq(Money.new(1.00))
|
278
|
+
end
|
279
|
+
|
280
|
+
it "supports to_i" do
|
281
|
+
expect(Money.new(1.50).to_i).to eq(1)
|
282
|
+
end
|
283
|
+
|
284
|
+
it "supports to_d" do
|
285
|
+
expect(Money.new(1.29).to_d).to eq(BigDecimal.new('1.29'))
|
286
|
+
end
|
287
|
+
|
288
|
+
it "supports to_f" do
|
289
|
+
expect(Money.new(1.50).to_f.to_s).to eq("1.5")
|
290
|
+
end
|
291
|
+
|
292
|
+
it "is creatable from an integer value in cents" do
|
293
|
+
expect(Money.from_cents(1950)).to eq(Money.new(19.50))
|
294
|
+
end
|
295
|
+
|
296
|
+
it "is creatable from an integer value of 0 in cents" do
|
297
|
+
expect(Money.from_cents(0)).to eq(Money.new)
|
298
|
+
end
|
299
|
+
|
300
|
+
it "is creatable from a float cents amount" do
|
301
|
+
expect(Money.from_cents(1950.5)).to eq(Money.new(19.51))
|
302
|
+
end
|
303
|
+
|
304
|
+
it "is creatable from an integer value in cents and currency" do
|
305
|
+
expect(Money.from_subunits(1950, 'CAD')).to eq(Money.new(19.50))
|
306
|
+
end
|
307
|
+
|
308
|
+
it "is creatable from an integer value in dollars and currency with no cents" do
|
309
|
+
expect(Money.from_subunits(1950, 'JPY')).to eq(Money.new(1950, 'JPY'))
|
310
|
+
end
|
311
|
+
|
312
|
+
it "raises when constructed with a NaN value" do
|
313
|
+
expect { Money.new( 0.0 / 0) }.to raise_error(ArgumentError)
|
314
|
+
end
|
315
|
+
|
316
|
+
it "is comparable with non-money objects" do
|
317
|
+
expect(money).not_to eq(nil)
|
318
|
+
end
|
319
|
+
|
320
|
+
it "supports floor" do
|
321
|
+
expect(Money.new(15.52).floor).to eq(Money.new(15.00))
|
322
|
+
expect(Money.new(18.99).floor).to eq(Money.new(18.00))
|
323
|
+
expect(Money.new(21).floor).to eq(Money.new(21))
|
324
|
+
end
|
325
|
+
|
326
|
+
it "generates a true rational" do
|
327
|
+
expect(Money.rational(Money.new(10.0), Money.new(15.0))).to eq(Rational(2,3))
|
328
|
+
expect(Money).to receive(:deprecate).once
|
329
|
+
expect(Money.rational(Money.new(10.0, 'USD'), Money.new(15.0, 'JPY'))).to eq(Rational(2,3))
|
330
|
+
end
|
331
|
+
|
332
|
+
describe "frozen with amount of $1" do
|
333
|
+
let (:money) { Money.new(1.00) }
|
334
|
+
|
335
|
+
it "is equals to $1" do
|
336
|
+
expect(money).to eq(Money.new(1.00))
|
337
|
+
end
|
338
|
+
|
339
|
+
it "is not equals to $2" do
|
340
|
+
expect(money).not_to eq(Money.new(2.00))
|
341
|
+
end
|
342
|
+
|
343
|
+
it "<=> $1 is 0" do
|
344
|
+
expect((money <=> Money.new(1.00))).to eq(0)
|
345
|
+
end
|
346
|
+
|
347
|
+
it "<=> $2 is -1" do
|
348
|
+
expect((money <=> Money.new(2.00))).to eq(-1)
|
349
|
+
end
|
350
|
+
|
351
|
+
it "<=> $0.50 equals 1" do
|
352
|
+
expect((money <=> Money.new(0.50))).to eq(1)
|
353
|
+
end
|
354
|
+
|
355
|
+
it "<=> works with non-money objects" do
|
356
|
+
expect((money <=> 1)).to eq(0)
|
357
|
+
expect((money <=> 2)).to eq(-1)
|
358
|
+
expect((money <=> 0.5)).to eq(1)
|
359
|
+
expect((1 <=> money)).to eq(0)
|
360
|
+
expect((2 <=> money)).to eq(1)
|
361
|
+
expect((0.5 <=> money)).to eq(-1)
|
362
|
+
end
|
363
|
+
|
364
|
+
it "raises error if compared other is not compatible" do
|
365
|
+
expect{ Money.new(5.00) <=> nil }.to raise_error(TypeError)
|
366
|
+
end
|
367
|
+
|
368
|
+
it "have the same hash value as $1" do
|
369
|
+
expect(money.hash).to eq(Money.new(1.00).hash)
|
370
|
+
end
|
371
|
+
|
372
|
+
it "does not have the same hash value as $2" do
|
373
|
+
expect(money.hash).to eq(Money.new(1.00).hash)
|
374
|
+
end
|
375
|
+
|
376
|
+
it "<=> can compare with and without currency" do
|
377
|
+
expect(Money.new(1000, Money::NULL_CURRENCY) <=> Money.new(2000, 'JPY')).to eq(-1)
|
378
|
+
expect(Money.new(2000, 'JPY') <=> Money.new(1000, Money::NULL_CURRENCY)).to eq(1)
|
379
|
+
end
|
380
|
+
|
381
|
+
it "<=> issues deprecation warning when comparing incompatible currency" do
|
382
|
+
expect(Money).to receive(:deprecate).twice
|
383
|
+
expect(Money.new(1000, 'USD') <=> Money.new(2000, 'JPY')).to eq(-1)
|
384
|
+
expect(Money.new(2000, 'JPY') <=> Money.new(1000, 'USD')).to eq(1)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
describe "#subunits" do
|
389
|
+
it 'multiplies by the number of decimal places for the currency' do
|
390
|
+
expect(Money.new(1, 'USD').subunits).to eq(100)
|
391
|
+
expect(Money.new(1, 'JPY').subunits).to eq(1)
|
392
|
+
expect(Money.new(1, 'IQD').subunits).to eq(1000)
|
393
|
+
expect(Money.new(1).subunits).to eq(100)
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
describe "value" do
|
398
|
+
it "rounds to the number of minor units provided by the currency" do
|
399
|
+
expect(Money.new(1.1111, 'USD').value).to eq(1.11)
|
400
|
+
expect(Money.new(1.1111, 'JPY').value).to eq(1)
|
401
|
+
expect(Money.new(1.1111, 'IQD').value).to eq(1.111)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
describe "with amount of $0" do
|
406
|
+
let (:money) { Money.new(0) }
|
407
|
+
|
408
|
+
it "is zero" do
|
409
|
+
expect(money).to be_zero
|
410
|
+
end
|
411
|
+
|
412
|
+
it "is greater than -$1" do
|
413
|
+
expect(money).to be > Money.new("-1.00")
|
414
|
+
end
|
415
|
+
|
416
|
+
it "is greater than or equal to $0" do
|
417
|
+
expect(money).to be >= Money.new
|
418
|
+
end
|
419
|
+
|
420
|
+
it "is less than or equal to $0" do
|
421
|
+
expect(money).to be <= Money.new
|
422
|
+
end
|
423
|
+
|
424
|
+
it "is less than $1" do
|
425
|
+
expect(money).to be < Money.new(1.00)
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
describe "with amount of $1" do
|
430
|
+
let (:money) { Money.new(1.00) }
|
431
|
+
|
432
|
+
it "is not zero" do
|
433
|
+
expect(money).not_to be_zero
|
434
|
+
end
|
435
|
+
|
436
|
+
it "returns cents as a decimal value = 1.00" do
|
437
|
+
expect(money.value).to eq(BigDecimal.new("1.00"))
|
438
|
+
end
|
439
|
+
|
440
|
+
it "returns cents as 100 cents" do
|
441
|
+
expect(money.cents).to eq(100)
|
442
|
+
end
|
443
|
+
|
444
|
+
it "returns cents as 100 cents" do
|
445
|
+
expect(money.subunits).to eq(100)
|
446
|
+
end
|
447
|
+
|
448
|
+
it "returns cents as a Fixnum" do
|
449
|
+
expect(money.subunits).to be_an_instance_of(Fixnum)
|
450
|
+
end
|
451
|
+
|
452
|
+
it "is greater than $0" do
|
453
|
+
expect(money).to be > Money.new(0.00)
|
454
|
+
end
|
455
|
+
|
456
|
+
it "is less than $2" do
|
457
|
+
expect(money).to be < Money.new(2.00)
|
458
|
+
end
|
459
|
+
|
460
|
+
it "is equal to $1" do
|
461
|
+
expect(money).to eq(Money.new(1.00))
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
describe "allocation"do
|
466
|
+
specify "#allocate takes no action when one gets all" do
|
467
|
+
expect(Money.new(5).allocate([1])).to eq([Money.new(5)])
|
468
|
+
end
|
469
|
+
|
470
|
+
specify "#allocate does not lose pennies" do
|
471
|
+
moneys = Money.new(0.05).allocate([0.3,0.7])
|
472
|
+
expect(moneys[0]).to eq(Money.new(0.02))
|
473
|
+
expect(moneys[1]).to eq(Money.new(0.03))
|
474
|
+
end
|
475
|
+
|
476
|
+
specify "#allocate does not lose dollars with non-decimal currency" do
|
477
|
+
moneys = Money.new(5, 'JPY').allocate([0.3,0.7])
|
478
|
+
expect(moneys[0]).to eq(Money.new(2, 'JPY'))
|
479
|
+
expect(moneys[1]).to eq(Money.new(3, 'JPY'))
|
480
|
+
end
|
481
|
+
|
482
|
+
specify "#allocate does not lose dollars with three decimal currency" do
|
483
|
+
moneys = Money.new(0.005, 'JOD').allocate([0.3,0.7])
|
484
|
+
expect(moneys[0]).to eq(Money.new(0.002, 'JOD'))
|
485
|
+
expect(moneys[1]).to eq(Money.new(0.003, 'JOD'))
|
486
|
+
end
|
487
|
+
|
488
|
+
specify "#allocate does not lose pennies even when given a lossy split" do
|
489
|
+
moneys = Money.new(1).allocate([0.333,0.333, 0.333])
|
490
|
+
expect(moneys[0].subunits).to eq(34)
|
491
|
+
expect(moneys[1].subunits).to eq(33)
|
492
|
+
expect(moneys[2].subunits).to eq(33)
|
493
|
+
end
|
494
|
+
|
495
|
+
specify "#allocate requires total to be less than 1" do
|
496
|
+
expect { Money.new(0.05).allocate([0.5,0.6]) }.to raise_error(ArgumentError)
|
497
|
+
end
|
498
|
+
|
499
|
+
specify "#allocate will use rationals if provided" do
|
500
|
+
splits = [128400,20439,14589,14589,25936].map{ |num| Rational(num, 203953) } # sums to > 1 if converted to float
|
501
|
+
expect(Money.new(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)])
|
502
|
+
end
|
503
|
+
|
504
|
+
specify "#allocate will convert rationals with high precision" do
|
505
|
+
ratios = [Rational(1, 1), Rational(0)]
|
506
|
+
expect(Money.new("858993456.12").allocate(ratios)).to eq([Money.new("858993456.12"), Money.empty])
|
507
|
+
ratios = [Rational(1, 6), Rational(5, 6)]
|
508
|
+
expect(Money.new("3.00").allocate(ratios)).to eq([Money.new("0.50"), Money.new("2.50")])
|
509
|
+
end
|
510
|
+
|
511
|
+
specify "#allocate doesn't raise with weird negative rational ratios" do
|
512
|
+
rate = Rational(-5, 1201)
|
513
|
+
expect { Money.new(1).allocate([rate, 1 - rate]) }.not_to raise_error
|
514
|
+
end
|
515
|
+
|
516
|
+
specify "#allocate_max_amounts returns the weighted allocation without exceeding the maxima when there is room for the remainder" do
|
517
|
+
expect(
|
518
|
+
Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.75)]),
|
519
|
+
).to eq([Money.new(26), Money.new(4.75)])
|
520
|
+
end
|
521
|
+
|
522
|
+
specify "#allocate_max_amounts returns the weighted allocation without exceeding the maxima when there is room for the remainder with currency" do
|
523
|
+
expect(
|
524
|
+
Money.new(3075, 'JPY').allocate_max_amounts([Money.new(2600, 'JPY'), Money.new(475, 'JPY')]),
|
525
|
+
).to eq([Money.new(2600, 'JPY'), Money.new(475, 'JPY')])
|
526
|
+
end
|
527
|
+
|
528
|
+
specify "#allocate_max_amounts legal computation with no currency objects" do
|
529
|
+
expect(
|
530
|
+
Money.new(3075, 'JPY').allocate_max_amounts([2600, 475]),
|
531
|
+
).to eq([Money.new(2600, 'JPY'), Money.new(475, 'JPY')])
|
532
|
+
|
533
|
+
expect(
|
534
|
+
Money.new(3075, Money::NULL_CURRENCY).allocate_max_amounts([Money.new(2600, 'JPY'), Money.new(475, 'JPY')]),
|
535
|
+
).to eq([Money.new(2600, 'JPY'), Money.new(475, 'JPY')])
|
536
|
+
end
|
537
|
+
|
538
|
+
specify "#allocate_max_amounts illegal computation across currencies" do
|
539
|
+
expect {
|
540
|
+
Money.new(3075, 'USD').allocate_max_amounts([Money.new(2600, 'JPY'), Money.new(475, 'JPY')])
|
541
|
+
}.to raise_error(ArgumentError)
|
542
|
+
end
|
543
|
+
|
544
|
+
specify "#allocate_max_amounts drops the remainder when returning the weighted allocation without exceeding the maxima when there is no room for the remainder" do
|
545
|
+
expect(
|
546
|
+
Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.74)]),
|
547
|
+
).to eq([Money.new(26), Money.new(4.74)])
|
548
|
+
end
|
549
|
+
|
550
|
+
specify "#allocate_max_amounts returns the weighted allocation when there is no remainder" do
|
551
|
+
expect(
|
552
|
+
Money.new(30).allocate_max_amounts([Money.new(15), Money.new(15)]),
|
553
|
+
).to eq([Money.new(15), Money.new(15)])
|
554
|
+
end
|
555
|
+
|
556
|
+
specify "#allocate_max_amounts allocates the remainder round-robin when the maxima are not reached" do
|
557
|
+
expect(
|
558
|
+
Money.new(1).allocate_max_amounts([Money.new(33), Money.new(33), Money.new(33)]),
|
559
|
+
).to eq([Money.new(0.34), Money.new(0.33), Money.new(0.33)])
|
560
|
+
end
|
561
|
+
|
562
|
+
specify "#allocate_max_amounts allocates up to the maxima specified" do
|
563
|
+
expect(
|
564
|
+
Money.new(100).allocate_max_amounts([Money.new(5), Money.new(2)]),
|
565
|
+
).to eq([Money.new(5), Money.new(2)])
|
566
|
+
end
|
567
|
+
|
568
|
+
specify "#allocate_max_amounts supports all-zero maxima" do
|
569
|
+
expect(
|
570
|
+
Money.new(3).allocate_max_amounts([Money.empty, Money.empty, Money.empty]),
|
571
|
+
).to eq([Money.empty, Money.empty, Money.empty])
|
572
|
+
end
|
573
|
+
end
|
574
|
+
|
575
|
+
describe "split" do
|
576
|
+
specify "#split needs at least one party" do
|
577
|
+
expect {Money.new(1).split(0)}.to raise_error(ArgumentError)
|
578
|
+
expect {Money.new(1).split(-1)}.to raise_error(ArgumentError)
|
579
|
+
end
|
580
|
+
|
581
|
+
specify "#gives 1 cent to both people if we start with 2" do
|
582
|
+
expect(Money.new(0.02).split(2)).to eq([Money.new(0.01), Money.new(0.01)])
|
583
|
+
end
|
584
|
+
|
585
|
+
specify "#split may distribute no money to some parties if there isnt enough to go around" do
|
586
|
+
expect(Money.new(0.02).split(3)).to eq([Money.new(0.01), Money.new(0.01), Money.new(0)])
|
587
|
+
end
|
588
|
+
|
589
|
+
specify "#split does not lose pennies" do
|
590
|
+
expect(Money.new(0.05).split(2)).to eq([Money.new(0.03), Money.new(0.02)])
|
591
|
+
end
|
592
|
+
|
593
|
+
specify "#split does not lose dollars with non-decimal currencies" do
|
594
|
+
expect(Money.new(5, 'JPY').split(2)).to eq([Money.new(3, 'JPY'), Money.new(2, 'JPY')])
|
595
|
+
end
|
596
|
+
|
597
|
+
specify "#split a dollar" do
|
598
|
+
moneys = Money.new(1).split(3)
|
599
|
+
expect(moneys[0].subunits).to eq(34)
|
600
|
+
expect(moneys[1].subunits).to eq(33)
|
601
|
+
expect(moneys[2].subunits).to eq(33)
|
602
|
+
end
|
603
|
+
|
604
|
+
specify "#split a 100 yen" do
|
605
|
+
moneys = Money.new(100, 'JPY').split(3)
|
606
|
+
expect(moneys[0].value).to eq(34)
|
607
|
+
expect(moneys[1].value).to eq(33)
|
608
|
+
expect(moneys[2].value).to eq(33)
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
describe "fraction" do
|
613
|
+
specify "#fraction needs a positive rate" do
|
614
|
+
expect {Money.new(1).fraction(-0.5)}.to raise_error(ArgumentError)
|
615
|
+
end
|
616
|
+
|
617
|
+
specify "#fraction returns the amount minus a fraction" do
|
618
|
+
expect(Money.new(1.15).fraction(0.15)).to eq(Money.new(1.00))
|
619
|
+
expect(Money.new(2.50).fraction(0.15)).to eq(Money.new(2.17))
|
620
|
+
expect(Money.new(35.50).fraction(0)).to eq(Money.new(35.50))
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
describe "with amount of $1 with created with 3 decimal places" do
|
625
|
+
let (:money) { Money.new(1.125) }
|
626
|
+
|
627
|
+
it "rounds 3rd decimal place" do
|
628
|
+
expect(money.value).to eq(BigDecimal.new("1.13"))
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
describe "parser dependency injection" do
|
633
|
+
before(:each) { Money.parser = AccountingMoneyParser }
|
634
|
+
after(:each) { Money.parser = MoneyParser }
|
635
|
+
|
636
|
+
it "keeps AccountingMoneyParser class on new money objects" do
|
637
|
+
expect(Money.new.class.parser).to eq(AccountingMoneyParser)
|
638
|
+
end
|
639
|
+
|
640
|
+
it "supports parenthesis from AccountingMoneyParser" do
|
641
|
+
expect(Money.parse("($5.00)")).to eq(Money.new(-5))
|
642
|
+
end
|
643
|
+
|
644
|
+
it "supports parenthesis from AccountingMoneyParser for .to_money" do
|
645
|
+
expect("($5.00)".to_money).to eq(Money.new(-5))
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
describe "round" do
|
650
|
+
|
651
|
+
it "rounds to 0 decimal places by default" do
|
652
|
+
expect(Money.new(54.1).round).to eq(Money.new(54))
|
653
|
+
expect(Money.new(54.5).round).to eq(Money.new(55))
|
654
|
+
end
|
655
|
+
|
656
|
+
# Overview of standard vs. banker's rounding for next 4 specs:
|
657
|
+
# http://www.xbeat.net/vbspeed/i_BankersRounding.htm
|
658
|
+
it "implements standard rounding for 2 digits" do
|
659
|
+
expect(Money.new(54.1754).round(2)).to eq(Money.new(54.18))
|
660
|
+
expect(Money.new(343.2050).round(2)).to eq(Money.new(343.21))
|
661
|
+
expect(Money.new(106.2038).round(2)).to eq(Money.new(106.20))
|
662
|
+
end
|
663
|
+
|
664
|
+
it "implements standard rounding for 1 digit" do
|
665
|
+
expect(Money.new(27.25).round(1)).to eq(Money.new(27.3))
|
666
|
+
expect(Money.new(27.45).round(1)).to eq(Money.new(27.5))
|
667
|
+
expect(Money.new(27.55).round(1)).to eq(Money.new(27.6))
|
668
|
+
end
|
669
|
+
|
670
|
+
end
|
671
|
+
|
672
|
+
describe "from_amount quacks like RubyMoney" do
|
673
|
+
it "accepts numeric values" do
|
674
|
+
expect(Money.from_amount(1)).to eq Money.from_cents(1_00)
|
675
|
+
expect(Money.from_amount(1.0)).to eq Money.from_cents(1_00)
|
676
|
+
expect(Money.from_amount(BigDecimal.new("1"))).to eq Money.from_cents(1_00)
|
677
|
+
end
|
678
|
+
|
679
|
+
it "accepts string values" do
|
680
|
+
expect(Money.from_amount("1")).to eq Money.from_cents(1_00)
|
681
|
+
end
|
682
|
+
|
683
|
+
it "accepts nil values" do
|
684
|
+
expect(Money.from_amount(nil)).to eq Money.from_cents(0)
|
685
|
+
end
|
686
|
+
|
687
|
+
it "accepts an optional currency parameter" do
|
688
|
+
expect { Money.from_amount(1, "CAD") }.to_not raise_error
|
689
|
+
end
|
690
|
+
|
691
|
+
it "accepts Rationla number" do
|
692
|
+
expect(Money.from_amount(Rational("999999999999999999.999")).value).to eql(BigDecimal.new("1000000000000000000", Money::Helpers::MAX_DECIMAL))
|
693
|
+
expect(Money.from_amount(Rational("999999999999999999.99")).value).to eql(BigDecimal.new("999999999999999999.99", Money::Helpers::MAX_DECIMAL))
|
694
|
+
end
|
695
|
+
|
696
|
+
it "raises ArgumentError with unsupported argument" do
|
697
|
+
expect { Money.from_amount(Object.new) }.to raise_error(ArgumentError)
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
describe "YAML serialization" do
|
702
|
+
it "accepts values with currencies" do
|
703
|
+
money = YAML.dump(Money.new(750, 'usd'))
|
704
|
+
expect(money).to eq("--- !ruby/object:Money\nvalue: '750.0'\ncurrency: USD\n")
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
describe "YAML deserialization" do
|
709
|
+
|
710
|
+
it "accepts values with currencies" do
|
711
|
+
money = YAML.load("--- !ruby/object:Money\nvalue: '750.0'\ncurrency: USD\n")
|
712
|
+
expect(money).to eq(Money.new(750, 'usd'))
|
713
|
+
end
|
714
|
+
|
715
|
+
it "accepts values with null currencies" do
|
716
|
+
money = YAML.load("--- !ruby/object:Money\nvalue: '750.0'\ncurrency: XXX\n")
|
717
|
+
expect(money).to eq(Money.new(750))
|
718
|
+
end
|
719
|
+
|
720
|
+
it "accepts serialized NullCurrency objects" do
|
721
|
+
money = YAML.load(<<~EOS)
|
722
|
+
---
|
723
|
+
!ruby/object:Money
|
724
|
+
currency: !ruby/object:Money::NullCurrency
|
725
|
+
symbol: >-
|
726
|
+
$
|
727
|
+
disambiguate_symbol:
|
728
|
+
iso_code: >-
|
729
|
+
XXX
|
730
|
+
iso_numeric: >-
|
731
|
+
999
|
732
|
+
name: >-
|
733
|
+
No Currency
|
734
|
+
smallest_denomination: 1
|
735
|
+
subunit_to_unit: 100
|
736
|
+
minor_units: 2
|
737
|
+
value: !ruby/object:BigDecimal 27:0.6935E2
|
738
|
+
cents: 6935
|
739
|
+
EOS
|
740
|
+
expect(money).to eq(Money.new(69.35, Money::NULL_CURRENCY))
|
741
|
+
end
|
742
|
+
|
743
|
+
it "accepts BigDecimal values" do
|
744
|
+
money = YAML.load(<<~EOS)
|
745
|
+
---
|
746
|
+
!ruby/object:Money
|
747
|
+
value: !ruby/object:BigDecimal 18:0.75E3
|
748
|
+
cents: 75000
|
749
|
+
EOS
|
750
|
+
expect(money).to be == Money.new(750)
|
751
|
+
expect(money.value).to be_a BigDecimal
|
752
|
+
end
|
753
|
+
|
754
|
+
it "accepts old float values..." do
|
755
|
+
money = YAML.load(<<~EOS)
|
756
|
+
---
|
757
|
+
!ruby/object:Money
|
758
|
+
value: 750.00
|
759
|
+
cents: 75000
|
760
|
+
EOS
|
761
|
+
expect(money).to be == Money.new(750)
|
762
|
+
expect(money.value).to be_a BigDecimal
|
763
|
+
end
|
764
|
+
end
|
765
|
+
|
766
|
+
describe('.deprecate') do
|
767
|
+
it "uses ruby warn if active support is not defined" do
|
768
|
+
stub_const("ACTIVE_SUPPORT_DEFINED", false)
|
769
|
+
expect(Kernel).to receive(:warn).once
|
770
|
+
Money.deprecate('ok')
|
771
|
+
end
|
772
|
+
|
773
|
+
it "uses active support warn if active support is defined" do
|
774
|
+
expect(Kernel).to receive(:warn).never
|
775
|
+
expect_any_instance_of(ActiveSupport::Deprecation).to receive(:warn).once
|
776
|
+
Money.deprecate('ok')
|
777
|
+
end
|
778
|
+
|
779
|
+
it "only sends a callstack of events outside of the money gem" do
|
780
|
+
expect_any_instance_of(ActiveSupport::Deprecation).to receive(:warn).with(
|
781
|
+
-> (message) { message == "[Shopify/Money] message\n" },
|
782
|
+
-> (callstack) { !callstack.first.to_s.include?('gems/money') && callstack.size > 0 }
|
783
|
+
)
|
784
|
+
Money.deprecate('message')
|
785
|
+
end
|
786
|
+
end
|
787
|
+
|
788
|
+
describe '#use_currency' do
|
789
|
+
it "allows setting the implicit default currency for a block scope" do
|
790
|
+
money = nil
|
791
|
+
Money.with_currency('CAD') do
|
792
|
+
money = Money.new(1.00)
|
793
|
+
end
|
794
|
+
|
795
|
+
expect(money.currency.iso_code).to eq('CAD')
|
796
|
+
end
|
797
|
+
|
798
|
+
it "does not use the currency for a block scope when explicitly set" do
|
799
|
+
money = nil
|
800
|
+
Money.with_currency('CAD') do
|
801
|
+
money = Money.new(1.00, 'USD')
|
802
|
+
end
|
803
|
+
|
804
|
+
expect(money.currency.iso_code).to eq('USD')
|
805
|
+
end
|
806
|
+
|
807
|
+
context "with .default_currency set" do
|
808
|
+
before(:each) { Money.default_currency = Money::Currency.new('EUR') }
|
809
|
+
after(:each) { Money.default_currency = Money::NULL_CURRENCY }
|
810
|
+
|
811
|
+
it "can be nested and falls back to default_currency outside of the blocks" do
|
812
|
+
money2, money3 = nil
|
813
|
+
|
814
|
+
money1 = Money.new(1.00)
|
815
|
+
Money.with_currency('CAD') do
|
816
|
+
Money.with_currency('USD') do
|
817
|
+
money2 = Money.new(1.00)
|
818
|
+
end
|
819
|
+
money3 = Money.new(1.00)
|
820
|
+
end
|
821
|
+
money4 = Money.new(1.00)
|
822
|
+
|
823
|
+
expect(money1.currency.iso_code).to eq('EUR')
|
824
|
+
expect(money2.currency.iso_code).to eq('USD')
|
825
|
+
expect(money3.currency.iso_code).to eq('CAD')
|
826
|
+
expect(money4.currency.iso_code).to eq('EUR')
|
827
|
+
end
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
describe '.clamp' do
|
832
|
+
let(:max) { 9000 }
|
833
|
+
let(:min) { -max }
|
834
|
+
|
835
|
+
it 'returns the same value if the value is within the min..max range' do
|
836
|
+
money = Money.new(5000, 'EUR').clamp(min, max)
|
837
|
+
expect(money.value).to eq(5000)
|
838
|
+
expect(money.currency.iso_code).to eq('EUR')
|
839
|
+
end
|
840
|
+
|
841
|
+
it 'returns the max value if the original value is larger' do
|
842
|
+
money = Money.new(9001, 'EUR').clamp(min, max)
|
843
|
+
expect(money.clamp(min, max).value).to eq(9000)
|
844
|
+
expect(money.clamp(min, max).currency.iso_code).to eq('EUR')
|
845
|
+
end
|
846
|
+
|
847
|
+
it 'returns the min value if the original value is smaller' do
|
848
|
+
money = Money.new(-9001, 'EUR').clamp(min, max)
|
849
|
+
expect(money.value).to eq(-9000)
|
850
|
+
expect(money.currency.iso_code).to eq('EUR')
|
851
|
+
end
|
852
|
+
end
|
853
|
+
end
|