money-joshm1 5.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +475 -0
- data/LICENSE +21 -0
- data/README.md +257 -0
- data/Rakefile +52 -0
- data/config/currency_backwards_compatible.json +128 -0
- data/config/currency_iso.json +2297 -0
- data/config/currency_non_iso.json +30 -0
- data/lib/money.rb +6 -0
- data/lib/money/bank/base.rb +130 -0
- data/lib/money/bank/variable_exchange.rb +252 -0
- data/lib/money/core_extensions.rb +82 -0
- data/lib/money/currency.rb +355 -0
- data/lib/money/currency/heuristics.rb +149 -0
- data/lib/money/currency/loader.rb +22 -0
- data/lib/money/money.rb +536 -0
- data/lib/money/money/arithmetic.rb +288 -0
- data/lib/money/money/formatting.rb +315 -0
- data/lib/money/money/parsing.rb +371 -0
- data/money.gemspec +29 -0
- data/spec/bank/base_spec.rb +77 -0
- data/spec/bank/variable_exchange_spec.rb +233 -0
- data/spec/core_extensions_spec.rb +160 -0
- data/spec/currency/heuristics_spec.rb +84 -0
- data/spec/currency_spec.rb +183 -0
- data/spec/money/arithmetic_spec.rb +598 -0
- data/spec/money/formatting_spec.rb +466 -0
- data/spec/money/parsing_spec.rb +309 -0
- data/spec/money_spec.rb +497 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/default_currency_helper.rb +13 -0
- metadata +145 -0
@@ -0,0 +1,309 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe Money, "parsing" do
|
6
|
+
|
7
|
+
bar = '{ "priority": 1, "iso_code": "BAR", "iso_numeric": "840", "name": "Dollar with 4 decimal places", "symbol": "$", "subunit": "Cent", "subunit_to_unit": 10000, "symbol_first": true, "html_entity": "$", "decimal_mark": ".", "thousands_separator": "," }'
|
8
|
+
eu4 = '{ "priority": 1, "iso_code": "EU4", "iso_numeric": "841", "name": "Euro with 4 decimal places", "symbol": "€", "subunit": "Cent", "subunit_to_unit": 10000, "symbol_first": true, "html_entity": "€", "decimal_mark": ",", "thousands_separator": "." }'
|
9
|
+
|
10
|
+
describe ".parse" do
|
11
|
+
it "parses european-formatted inputs under 10EUR" do
|
12
|
+
five_ninety_five = Money.new(595, 'EUR')
|
13
|
+
|
14
|
+
Money.parse('EUR 5,95').should == five_ninety_five
|
15
|
+
#TODO: try and handle these
|
16
|
+
#Money.parse('€5,95').should == five_ninety_five
|
17
|
+
#Money.parse('$5.95').should == five_ninety_five
|
18
|
+
end
|
19
|
+
|
20
|
+
it "parses european-formatted inputs with multiple thousands-seperators" do
|
21
|
+
Money.parse('EUR 1.234.567,89').should == Money.new(123456789, 'EUR')
|
22
|
+
Money.parse('EUR 1.111.234.567,89').should == Money.new(111123456789, 'EUR')
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'currency assumption' do
|
26
|
+
context 'opted in' do
|
27
|
+
before do
|
28
|
+
Money.assume_from_symbol = true
|
29
|
+
end
|
30
|
+
it "parses formatted inputs with the currency passed as a symbol" do
|
31
|
+
with_default_currency("EUR") do
|
32
|
+
Money.parse("$5.95").should == Money.new(595, 'USD')
|
33
|
+
end
|
34
|
+
Money.parse("€5.95").should == Money.new(595, 'EUR')
|
35
|
+
Money.parse(" €5.95 ").should == Money.new(595, 'EUR')
|
36
|
+
Money.parse("£9.99").should == Money.new(999, 'GBP')
|
37
|
+
end
|
38
|
+
it 'should assume default currency if not a recognised symbol' do
|
39
|
+
Money.parse("L9.99").should == Money.new(999, 'USD')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
context 'opted out' do
|
43
|
+
before do
|
44
|
+
Money.assume_from_symbol = false
|
45
|
+
end
|
46
|
+
it "parses formatted inputs with the currency passed as a symbol but ignores the symbol" do
|
47
|
+
Money.parse("$5.95").should == Money.new(595, 'USD')
|
48
|
+
Money.parse("€5.95").should == Money.new(595, 'USD')
|
49
|
+
Money.parse(" €5.95 ").should == Money.new(595, 'USD')
|
50
|
+
Money.parse("£9.99").should == Money.new(999, 'USD')
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
it 'should opt out by default' do
|
55
|
+
Money.assume_from_symbol.should be_false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it "parses USD-formatted inputs under $10" do
|
60
|
+
five_ninety_five = Money.new(595, 'USD')
|
61
|
+
|
62
|
+
Money.parse(5.95).should == five_ninety_five
|
63
|
+
Money.parse('5.95').should == five_ninety_five
|
64
|
+
Money.parse('$5.95').should == five_ninety_five
|
65
|
+
Money.parse("\n $5.95 \n").should == five_ninety_five
|
66
|
+
Money.parse('$ 5.95').should == five_ninety_five
|
67
|
+
Money.parse('$5.95 ea.').should == five_ninety_five
|
68
|
+
Money.parse('$5.95, each').should == five_ninety_five
|
69
|
+
end
|
70
|
+
|
71
|
+
it "parses USD-formatted inputs with multiple thousands-seperators" do
|
72
|
+
Money.parse('1,234,567.89').should == Money.new(123456789, 'USD')
|
73
|
+
Money.parse('1,111,234,567.89').should == Money.new(111123456789, 'USD')
|
74
|
+
end
|
75
|
+
|
76
|
+
it "does not return a price if there is a price range" do
|
77
|
+
expect { Money.parse('$5.95-10.95') }.to raise_error ArgumentError
|
78
|
+
expect { Money.parse('$5.95 - 10.95') }.to raise_error ArgumentError
|
79
|
+
expect { Money.parse('$5.95 - $10.95') }.to raise_error ArgumentError
|
80
|
+
end
|
81
|
+
|
82
|
+
it "does not return a price for completely invalid input" do
|
83
|
+
# TODO: shouldn't these throw an error instead of being considered
|
84
|
+
# equal to $0.0?
|
85
|
+
empty_price = Money.new(0, 'USD')
|
86
|
+
|
87
|
+
Money.parse(nil).should == empty_price
|
88
|
+
Money.parse('hellothere').should == empty_price
|
89
|
+
Money.parse('').should == empty_price
|
90
|
+
end
|
91
|
+
|
92
|
+
it "handles negative inputs" do
|
93
|
+
five_ninety_five = Money.new(595, 'USD')
|
94
|
+
|
95
|
+
Money.parse("$-5.95").should == -five_ninety_five
|
96
|
+
Money.parse("-$5.95").should == -five_ninety_five
|
97
|
+
Money.parse("$5.95-").should == -five_ninety_five
|
98
|
+
end
|
99
|
+
|
100
|
+
it "raises ArgumentError when unable to detect polarity" do
|
101
|
+
expect { Money.parse('-$5.95-') }.to raise_error ArgumentError
|
102
|
+
end
|
103
|
+
|
104
|
+
it "parses correctly strings with exactly 3 decimal digits" do
|
105
|
+
Money.parse("6,534", "EUR").should == Money.new(653, "EUR")
|
106
|
+
end
|
107
|
+
|
108
|
+
context "custom currencies with 4 decimal places" do
|
109
|
+
before :each do
|
110
|
+
Money::Currency.register(JSON.parse(bar, :symbolize_names => true))
|
111
|
+
Money::Currency.register(JSON.parse(eu4, :symbolize_names => true))
|
112
|
+
end
|
113
|
+
|
114
|
+
# String#to_money(Currency) is equivalent to Money.parse(String, Currency)
|
115
|
+
it "parses strings respecting subunit to unit, decimal and thousands separator" do
|
116
|
+
"$0.4".to_money("BAR").should == Money.new(4000, "BAR")
|
117
|
+
"€0,4".to_money("EU4").should == Money.new(4000, "EU4")
|
118
|
+
|
119
|
+
"$0.04".to_money("BAR").should == Money.new(400, "BAR")
|
120
|
+
"€0,04".to_money("EU4").should == Money.new(400, "EU4")
|
121
|
+
|
122
|
+
"$0.004".to_money("BAR").should == Money.new(40, "BAR")
|
123
|
+
"€0,004".to_money("EU4").should == Money.new(40, "EU4")
|
124
|
+
|
125
|
+
"$0.0004".to_money("BAR").should == Money.new(4, "BAR")
|
126
|
+
"€0,0004".to_money("EU4").should == Money.new(4, "EU4")
|
127
|
+
|
128
|
+
"$0.0024".to_money("BAR").should == Money.new(24, "BAR")
|
129
|
+
"€0,0024".to_money("EU4").should == Money.new(24, "EU4")
|
130
|
+
|
131
|
+
"$0.0324".to_money("BAR").should == Money.new(324, "BAR")
|
132
|
+
"€0,0324".to_money("EU4").should == Money.new(324, "EU4")
|
133
|
+
|
134
|
+
"$0.5324".to_money("BAR").should == Money.new(5324, "BAR")
|
135
|
+
"€0,5324".to_money("EU4").should == Money.new(5324, "EU4")
|
136
|
+
|
137
|
+
"$6.5324".to_money("BAR").should == Money.new(65324, "BAR")
|
138
|
+
"€6,5324".to_money("EU4").should == Money.new(65324, "EU4")
|
139
|
+
|
140
|
+
"$86.5324".to_money("BAR").should == Money.new(865324, "BAR")
|
141
|
+
"€86,5324".to_money("EU4").should == Money.new(865324, "EU4")
|
142
|
+
|
143
|
+
"$186.5324".to_money("BAR").should == Money.new(1865324, "BAR")
|
144
|
+
"€186,5324".to_money("EU4").should == Money.new(1865324, "EU4")
|
145
|
+
|
146
|
+
"$3,331.0034".to_money("BAR").should == Money.new(33310034, "BAR")
|
147
|
+
"€3.331,0034".to_money("EU4").should == Money.new(33310034, "EU4")
|
148
|
+
|
149
|
+
"$8,883,331.0034".to_money("BAR").should == Money.new(88833310034, "BAR")
|
150
|
+
"€8.883.331,0034".to_money("EU4").should == Money.new(88833310034, "EU4")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
describe ".from_string" do
|
156
|
+
it "converts given amount to cents" do
|
157
|
+
Money.from_string("1").should == Money.new(1_00)
|
158
|
+
Money.from_string("1").should == Money.new(1_00, "USD")
|
159
|
+
Money.from_string("1", "EUR").should == Money.new(1_00, "EUR")
|
160
|
+
end
|
161
|
+
|
162
|
+
it "respects :subunit_to_unit currency property" do
|
163
|
+
Money.from_string("1", "USD").should == Money.new(1_00, "USD")
|
164
|
+
Money.from_string("1", "TND").should == Money.new(1_000, "TND")
|
165
|
+
Money.from_string("1", "CLP").should == Money.new(1, "CLP")
|
166
|
+
end
|
167
|
+
|
168
|
+
it "accepts a currency options" do
|
169
|
+
m = Money.from_string("1")
|
170
|
+
m.currency.should == Money.default_currency
|
171
|
+
|
172
|
+
m = Money.from_string("1", Money::Currency.wrap("EUR"))
|
173
|
+
m.currency.should == Money::Currency.wrap("EUR")
|
174
|
+
|
175
|
+
m = Money.from_string("1", "EUR")
|
176
|
+
m.currency.should == Money::Currency.wrap("EUR")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
describe ".from_fixnum" do
|
181
|
+
it "converts given amount to cents" do
|
182
|
+
Money.from_fixnum(1).should == Money.new(1_00)
|
183
|
+
Money.from_fixnum(1).should == Money.new(1_00, "USD")
|
184
|
+
Money.from_fixnum(1, "EUR").should == Money.new(1_00, "EUR")
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should respect :subunit_to_unit currency property" do
|
188
|
+
Money.from_fixnum(1, "USD").should == Money.new(1_00, "USD")
|
189
|
+
Money.from_fixnum(1, "TND").should == Money.new(1_000, "TND")
|
190
|
+
Money.from_fixnum(1, "CLP").should == Money.new(1, "CLP")
|
191
|
+
end
|
192
|
+
|
193
|
+
it "accepts a currency options" do
|
194
|
+
m = Money.from_fixnum(1)
|
195
|
+
m.currency.should == Money.default_currency
|
196
|
+
|
197
|
+
m = Money.from_fixnum(1, Money::Currency.wrap("EUR"))
|
198
|
+
m.currency.should == Money::Currency.wrap("EUR")
|
199
|
+
|
200
|
+
m = Money.from_fixnum(1, "EUR")
|
201
|
+
m.currency.should == Money::Currency.wrap("EUR")
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe ".from_float" do
|
206
|
+
it "converts given amount to cents" do
|
207
|
+
Money.from_float(1.2).should == Money.new(1_20)
|
208
|
+
Money.from_float(1.2).should == Money.new(1_20, "USD")
|
209
|
+
Money.from_float(1.2, "EUR").should == Money.new(1_20, "EUR")
|
210
|
+
end
|
211
|
+
|
212
|
+
it "respects :subunit_to_unit currency property" do
|
213
|
+
Money.from_float(1.2, "USD").should == Money.new(1_20, "USD")
|
214
|
+
Money.from_float(1.2, "TND").should == Money.new(1_200, "TND")
|
215
|
+
Money.from_float(1.2, "CLP").should == Money.new(1, "CLP")
|
216
|
+
end
|
217
|
+
|
218
|
+
it "accepts a currency options" do
|
219
|
+
m = Money.from_float(1.2)
|
220
|
+
m.currency.should == Money.default_currency
|
221
|
+
|
222
|
+
m = Money.from_float(1.2, Money::Currency.wrap("EUR"))
|
223
|
+
m.currency.should == Money::Currency.wrap("EUR")
|
224
|
+
|
225
|
+
m = Money.from_float(1.2, "EUR")
|
226
|
+
m.currency.should == Money::Currency.wrap("EUR")
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
describe ".from_bigdecimal" do
|
231
|
+
it "converts given amount to cents" do
|
232
|
+
Money.from_bigdecimal(BigDecimal.new("1")).should == Money.new(1_00)
|
233
|
+
Money.from_bigdecimal(BigDecimal.new("1")).should == Money.new(1_00, "USD")
|
234
|
+
Money.from_bigdecimal(BigDecimal.new("1"), "EUR").should == Money.new(1_00, "EUR")
|
235
|
+
end
|
236
|
+
|
237
|
+
it "respects :subunit_to_unit currency property" do
|
238
|
+
Money.from_bigdecimal(BigDecimal.new("1"), "USD").should == Money.new(1_00, "USD")
|
239
|
+
Money.from_bigdecimal(BigDecimal.new("1"), "TND").should == Money.new(1_000, "TND")
|
240
|
+
Money.from_bigdecimal(BigDecimal.new("1"), "CLP").should == Money.new(1, "CLP")
|
241
|
+
end
|
242
|
+
|
243
|
+
it "accepts a currency options" do
|
244
|
+
m = Money.from_bigdecimal(BigDecimal.new("1"))
|
245
|
+
m.currency.should == Money.default_currency
|
246
|
+
|
247
|
+
m = Money.from_bigdecimal(BigDecimal.new("1"), Money::Currency.wrap("EUR"))
|
248
|
+
m.currency.should == Money::Currency.wrap("EUR")
|
249
|
+
|
250
|
+
m = Money.from_bigdecimal(BigDecimal.new("1"), "EUR")
|
251
|
+
m.currency.should == Money::Currency.wrap("EUR")
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
describe ".from_numeric" do
|
256
|
+
it "converts given amount to cents" do
|
257
|
+
Money.from_numeric(1).should == Money.new(1_00)
|
258
|
+
Money.from_numeric(1.0).should == Money.new(1_00)
|
259
|
+
Money.from_numeric(BigDecimal.new("1")).should == Money.new(1_00)
|
260
|
+
end
|
261
|
+
|
262
|
+
it "raises ArgumentError with unsupported argument" do
|
263
|
+
expect { Money.from_numeric("100") }.to raise_error(ArgumentError)
|
264
|
+
end
|
265
|
+
|
266
|
+
it "optimizes workload" do
|
267
|
+
Money.should_receive(:from_fixnum).with(1, "USD").and_return(Money.new(1_00, "USD"))
|
268
|
+
Money.from_numeric(1, "USD").should == Money.new(1_00, "USD")
|
269
|
+
Money.should_receive(:from_bigdecimal).with(BigDecimal.new("1.0"), "USD").and_return(Money.new(1_00, "USD"))
|
270
|
+
Money.from_numeric(1.0, "USD").should == Money.new(1_00, "USD")
|
271
|
+
end
|
272
|
+
|
273
|
+
it "respects :subunit_to_unit currency property" do
|
274
|
+
Money.from_numeric(1, "USD").should == Money.new(1_00, "USD")
|
275
|
+
Money.from_numeric(1, "TND").should == Money.new(1_000, "TND")
|
276
|
+
Money.from_numeric(1, "CLP").should == Money.new(1, "CLP")
|
277
|
+
end
|
278
|
+
|
279
|
+
it "accepts a bank option" do
|
280
|
+
Money.from_numeric(1).should == Money.new(1_00)
|
281
|
+
Money.from_numeric(1).should == Money.new(1_00, "USD")
|
282
|
+
Money.from_numeric(1, "EUR").should == Money.new(1_00, "EUR")
|
283
|
+
end
|
284
|
+
|
285
|
+
it "accepts a currency options" do
|
286
|
+
m = Money.from_numeric(1)
|
287
|
+
m.currency.should == Money.default_currency
|
288
|
+
|
289
|
+
m = Money.from_numeric(1, Money::Currency.wrap("EUR"))
|
290
|
+
m.currency.should == Money::Currency.wrap("EUR")
|
291
|
+
|
292
|
+
m = Money.from_numeric(1, "EUR")
|
293
|
+
m.currency.should == Money::Currency.wrap("EUR")
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
describe ".extract_cents" do
|
298
|
+
it "correctly treats pipe marks '|' in input (regression test)" do
|
299
|
+
Money.extract_cents('100|0').should == Money.extract_cents('100!0')
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
context "given the same inputs to .parse and .from_*" do
|
304
|
+
it "gives the same results" do
|
305
|
+
4.635.to_money.should == "4.635".to_money
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
end
|
data/spec/money_spec.rb
ADDED
@@ -0,0 +1,497 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe Money do
|
6
|
+
describe ".new" do
|
7
|
+
it "rounds the given cents to an integer" do
|
8
|
+
Money.new(1.00, "USD").cents.should == 1
|
9
|
+
Money.new(1.01, "USD").cents.should == 1
|
10
|
+
Money.new(1.50, "USD").cents.should == 2
|
11
|
+
end
|
12
|
+
|
13
|
+
it "is associated to the singleton instance of Bank::VariableExchange by default" do
|
14
|
+
Money.new(0).bank.should be(Money::Bank::VariableExchange.instance)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "handles Rationals" do
|
18
|
+
n = Rational(1)
|
19
|
+
Money.new(n).cents.should == 1
|
20
|
+
end
|
21
|
+
|
22
|
+
it "handles Floats" do
|
23
|
+
n = Float("1")
|
24
|
+
Money.new(n).cents.should == 1
|
25
|
+
end
|
26
|
+
|
27
|
+
context "infinite_precision = true" do
|
28
|
+
before do
|
29
|
+
Money.infinite_precision = true
|
30
|
+
end
|
31
|
+
|
32
|
+
after do
|
33
|
+
Money.infinite_precision = false
|
34
|
+
end
|
35
|
+
|
36
|
+
it "doesn't round cents" do
|
37
|
+
Money.new(1.01, "USD").cents.should == BigDecimal("1.01")
|
38
|
+
Money.new(1.50, "USD").cents.should == BigDecimal("1.50")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe ".new_with_dollars" do
|
44
|
+
it "is synonym of #new_with_amount" do
|
45
|
+
MoneyExpectation = Class.new(Money)
|
46
|
+
def MoneyExpectation.new_with_amount *args
|
47
|
+
args
|
48
|
+
end
|
49
|
+
MoneyExpectation.new_with_dollars("expectation").should == ["expectation"]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe ".new_with_amount" do
|
54
|
+
it "converts given amount to cents" do
|
55
|
+
Money.new_with_amount(1).should == Money.new(100)
|
56
|
+
Money.new_with_amount(1, "USD").should == Money.new(100, "USD")
|
57
|
+
Money.new_with_amount(1, "EUR").should == Money.new(100, "EUR")
|
58
|
+
end
|
59
|
+
|
60
|
+
it "respects :subunit_to_unit currency property" do
|
61
|
+
Money.new_with_amount(1, "USD").should == Money.new(1_00, "USD")
|
62
|
+
Money.new_with_amount(1, "TND").should == Money.new(1_000, "TND")
|
63
|
+
Money.new_with_amount(1, "CLP").should == Money.new(1, "CLP")
|
64
|
+
end
|
65
|
+
|
66
|
+
it "does not loose precision" do
|
67
|
+
Money.new_with_amount(1234).cents.should == 1234_00
|
68
|
+
Money.new_with_amount(100.37).cents.should == 100_37
|
69
|
+
Money.new_with_amount(BigDecimal.new('1234')).cents.should == 1234_00
|
70
|
+
end
|
71
|
+
|
72
|
+
it "accepts optional currency" do
|
73
|
+
m = Money.new_with_amount(1)
|
74
|
+
m.currency.should == Money.default_currency
|
75
|
+
|
76
|
+
m = Money.new_with_amount(1, Money::Currency.wrap("EUR"))
|
77
|
+
m.currency.should == Money::Currency.wrap("EUR")
|
78
|
+
|
79
|
+
m = Money.new_with_amount(1, "EUR")
|
80
|
+
m.currency.should == Money::Currency.wrap("EUR")
|
81
|
+
end
|
82
|
+
|
83
|
+
it "accepts optional bank" do
|
84
|
+
m = Money.new_with_amount(1)
|
85
|
+
m.bank.should == Money.default_bank
|
86
|
+
|
87
|
+
m = Money.new_with_amount(1, "EUR", bank = Object.new)
|
88
|
+
m.bank.should == bank
|
89
|
+
end
|
90
|
+
|
91
|
+
it "is associated to the singleton instance of Bank::VariableExchange by default" do
|
92
|
+
Money.new_with_amount(0).bank.should be(Money::Bank::VariableExchange.instance)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe ".empty" do
|
97
|
+
it "creates a new Money object of 0 cents" do
|
98
|
+
Money.empty.should == Money.new(0)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe ".ca_dollar" do
|
103
|
+
it "creates a new Money object of the given value in CAD" do
|
104
|
+
Money.ca_dollar(50).should == Money.new(50, "CAD")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe ".us_dollar" do
|
109
|
+
it "creates a new Money object of the given value in USD" do
|
110
|
+
Money.us_dollar(50).should == Money.new(50, "USD")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe ".euro" do
|
115
|
+
it "creates a new Money object of the given value in EUR" do
|
116
|
+
Money.euro(50).should == Money.new(50, "EUR")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe ".add_rate" do
|
121
|
+
before do
|
122
|
+
@default_bank = Money.default_bank
|
123
|
+
Money.default_bank = Money::Bank::VariableExchange.new
|
124
|
+
end
|
125
|
+
|
126
|
+
after do
|
127
|
+
Money.default_bank = @default_bank
|
128
|
+
end
|
129
|
+
|
130
|
+
it "saves rate into current bank" do
|
131
|
+
Money.add_rate("EUR", "USD", 10)
|
132
|
+
Money.new(10_00, "EUR").exchange_to("USD").should == Money.new(100_00, "USD")
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "#cents" do
|
137
|
+
it "is a synonym of #fractional" do
|
138
|
+
expectation = Money.new(0)
|
139
|
+
def expectation.fractional
|
140
|
+
"expectation"
|
141
|
+
end
|
142
|
+
expectation.cents.should == "expectation"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "#fractional" do
|
147
|
+
it "returns the amount in fractional unit" do
|
148
|
+
Money.new(1_00).fractional.should == 1_00
|
149
|
+
Money.new_with_amount(1).fractional.should == 1_00
|
150
|
+
end
|
151
|
+
|
152
|
+
it "stores fractional as an integer regardless of what is passed into the constructor" do
|
153
|
+
[ Money.new(100), 1.to_money, 1.00.to_money, BigDecimal('1.00').to_money ].each do |m|
|
154
|
+
m.fractional.should == 100
|
155
|
+
m.fractional.should be_a(Fixnum)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
context "loading a serialized Money via YAML" do
|
160
|
+
it "uses BigDecimal when rounding" do
|
161
|
+
serialized = <<YAML
|
162
|
+
!ruby/object:Money
|
163
|
+
fractional: 249.5
|
164
|
+
currency: !ruby/object:Money::Currency
|
165
|
+
id: :eur
|
166
|
+
priority: 2
|
167
|
+
iso_code: EUR
|
168
|
+
name: Euro
|
169
|
+
symbol: €
|
170
|
+
alternate_symbols: []
|
171
|
+
subunit: Cent
|
172
|
+
subunit_to_unit: 100
|
173
|
+
symbol_first: true
|
174
|
+
html_entity: ! '€'
|
175
|
+
decimal_mark: ! ','
|
176
|
+
thousands_separator: .
|
177
|
+
iso_numeric: '978'
|
178
|
+
mutex: !ruby/object:Mutex {}
|
179
|
+
last_updated: 2012-11-23 20:41:47.454438399 +02:00
|
180
|
+
YAML
|
181
|
+
m = YAML::load serialized
|
182
|
+
m.should be_a(Money)
|
183
|
+
m.class.infinite_precision.should == false
|
184
|
+
m.fractional.should == 250 # 249.5 rounded up
|
185
|
+
m.fractional.should be_a(Integer)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
context "user changes rounding_mode" do
|
190
|
+
after do
|
191
|
+
Money.rounding_mode = BigDecimal::ROUND_HALF_EVEN
|
192
|
+
end
|
193
|
+
|
194
|
+
it "respects the rounding_mode" do
|
195
|
+
Money.rounding_mode = BigDecimal::ROUND_DOWN
|
196
|
+
Money.new(1.9).fractional.should == 1
|
197
|
+
|
198
|
+
Money.rounding_mode = BigDecimal::ROUND_UP
|
199
|
+
Money.new(1.1).fractional.should == 2
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
context "infinite_precision = true" do
|
204
|
+
before do
|
205
|
+
Money.infinite_precision = true
|
206
|
+
end
|
207
|
+
|
208
|
+
after do
|
209
|
+
Money.infinite_precision = false
|
210
|
+
end
|
211
|
+
|
212
|
+
it "returns the amount in fractional unit" do
|
213
|
+
Money.new(1_00).fractional.should == BigDecimal("100")
|
214
|
+
Money.new_with_amount(1).fractional.should == BigDecimal("100")
|
215
|
+
end
|
216
|
+
|
217
|
+
it "stores in fractional unit as an integer regardless of what is passed into the constructor" do
|
218
|
+
[ Money.new(100), 1.to_money, 1.00.to_money, BigDecimal('1.00').to_money ].each do |m|
|
219
|
+
m.fractional.should == BigDecimal("100")
|
220
|
+
m.fractional.should be_a(BigDecimal)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
describe "#amount" do
|
227
|
+
it "returns the amount of cents as dollars" do
|
228
|
+
Money.new(1_00).amount.should == 1
|
229
|
+
Money.new_with_amount(1).amount.should == 1
|
230
|
+
end
|
231
|
+
|
232
|
+
it "respects :subunit_to_unit currency property" do
|
233
|
+
Money.new(1_00, "USD").amount.should == 1
|
234
|
+
Money.new(1_000, "TND").amount.should == 1
|
235
|
+
Money.new(1, "CLP").amount.should == 1
|
236
|
+
end
|
237
|
+
|
238
|
+
it "does not loose precision" do
|
239
|
+
Money.new(100_37).amount.should == 100.37
|
240
|
+
Money.new_with_amount(100.37).amount.should == 100.37
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
describe "#dollars" do
|
245
|
+
it "is synonym of #amount" do
|
246
|
+
m = Money.new(0)
|
247
|
+
|
248
|
+
# Make a small expectation
|
249
|
+
def m.amount
|
250
|
+
5
|
251
|
+
end
|
252
|
+
|
253
|
+
m.dollars.should == 5
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
describe "#currency" do
|
258
|
+
it "returns the currency object" do
|
259
|
+
Money.new(1_00, "USD").currency.should == Money::Currency.new("USD")
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
describe "#currency_as_string" do
|
264
|
+
it "returns the iso_code of the currency object" do
|
265
|
+
Money.new(1_00, "USD").currency_as_string.should == "USD"
|
266
|
+
Money.new(1_00, "EUR").currency_as_string.should == "EUR"
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
describe "#currency_as_string=" do
|
271
|
+
it "sets the currency object using the provided string" do
|
272
|
+
money = Money.new(100_00, "USD")
|
273
|
+
money.currency_as_string = "EUR"
|
274
|
+
money.currency.should == Money::Currency.new("EUR")
|
275
|
+
money.currency_as_string = "YEN"
|
276
|
+
money.currency.should == Money::Currency.new("YEN")
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
describe "#hash=" do
|
281
|
+
it "returns the same value for equal objects" do
|
282
|
+
Money.new(1_00, "EUR").hash.should == Money.new(1_00, "EUR").hash
|
283
|
+
Money.new(2_00, "USD").hash.should == Money.new(2_00, "USD").hash
|
284
|
+
Money.new(1_00, "EUR").hash.should_not == Money.new(2_00, "EUR").hash
|
285
|
+
Money.new(1_00, "EUR").hash.should_not == Money.new(1_00, "USD").hash
|
286
|
+
Money.new(1_00, "EUR").hash.should_not == Money.new(2_00, "USD").hash
|
287
|
+
end
|
288
|
+
|
289
|
+
it "can be used to return the intersection of Money object arrays" do
|
290
|
+
intersection = [Money.new(1_00, "EUR"), Money.new(1_00, "USD")] & [Money.new(1_00, "EUR")]
|
291
|
+
intersection.should == [Money.new(1_00, "EUR")]
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
describe "#symbol" do
|
296
|
+
it "works as documented" do
|
297
|
+
currency = Money::Currency.new("EUR")
|
298
|
+
currency.should_receive(:symbol).and_return("€")
|
299
|
+
Money.empty(currency).symbol.should == "€"
|
300
|
+
|
301
|
+
currency = Money::Currency.new("EUR")
|
302
|
+
currency.should_receive(:symbol).and_return(nil)
|
303
|
+
Money.empty(currency).symbol.should == "¤"
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
describe "#to_s" do
|
308
|
+
it "works as documented" do
|
309
|
+
Money.new(10_00).to_s.should == "10.00"
|
310
|
+
Money.new(400_08).to_s.should == "400.08"
|
311
|
+
Money.new(-237_43).to_s.should == "-237.43"
|
312
|
+
end
|
313
|
+
|
314
|
+
it "respects :subunit_to_unit currency property" do
|
315
|
+
Money.new(10_00, "BHD").to_s.should == "1.000"
|
316
|
+
Money.new(10_00, "CNY").to_s.should == "10.00"
|
317
|
+
end
|
318
|
+
|
319
|
+
it "does not have decimal when :subunit_to_unit == 1" do
|
320
|
+
Money.new(10_00, "CLP").to_s.should == "1000"
|
321
|
+
end
|
322
|
+
|
323
|
+
it "does not work when :subunit_to_unit == 5" do
|
324
|
+
Money.new(10_00, "MGA").to_s.should == "200.0"
|
325
|
+
end
|
326
|
+
|
327
|
+
it "respects :decimal_mark" do
|
328
|
+
Money.new(10_00, "BRL").to_s.should == "10,00"
|
329
|
+
end
|
330
|
+
|
331
|
+
context "infinite_precision = true" do
|
332
|
+
before do
|
333
|
+
Money.infinite_precision = true
|
334
|
+
end
|
335
|
+
|
336
|
+
after do
|
337
|
+
Money.infinite_precision = false
|
338
|
+
end
|
339
|
+
|
340
|
+
it "shows fractional cents" do
|
341
|
+
Money.new(1.05, "USD").to_s.should == "0.0105"
|
342
|
+
end
|
343
|
+
|
344
|
+
it "suppresses fractional cents when there is none" do
|
345
|
+
Money.new(1.0, "USD").to_s.should == "0.01"
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
describe "#to_d" do
|
351
|
+
it "works as documented" do
|
352
|
+
decimal = Money.new(10_00).to_d
|
353
|
+
decimal.should be_a(BigDecimal)
|
354
|
+
decimal.should == 10.0
|
355
|
+
end
|
356
|
+
|
357
|
+
it "respects :subunit_to_unit currency property" do
|
358
|
+
decimal = Money.new(10_00, "BHD").to_d
|
359
|
+
decimal.should be_a(BigDecimal)
|
360
|
+
decimal.should == 1.0
|
361
|
+
end
|
362
|
+
|
363
|
+
it "works with float :subunit_to_unit currency property" do
|
364
|
+
money = Money.new(10_00, "BHD")
|
365
|
+
money.currency.stub(:subunit_to_unit).and_return(1000.0)
|
366
|
+
|
367
|
+
decimal = money.to_d
|
368
|
+
decimal.should be_a(BigDecimal)
|
369
|
+
decimal.should == 1.0
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
describe "#to_f" do
|
374
|
+
it "works as documented" do
|
375
|
+
Money.new(10_00).to_f.should == 10.0
|
376
|
+
end
|
377
|
+
|
378
|
+
it "respects :subunit_to_unit currency property" do
|
379
|
+
Money.new(10_00, "BHD").to_f.should == 1.0
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
describe "#to_money" do
|
384
|
+
it "works as documented" do
|
385
|
+
money = Money.new(10_00, "DKK")
|
386
|
+
money.should == money.to_money
|
387
|
+
money.should == money.to_money("DKK")
|
388
|
+
money.bank.should_receive(:exchange_with).with(Money.new(10_00, Money::Currency.new("DKK")), Money::Currency.new("EUR")).and_return(Money.new(200_00, Money::Currency.new('EUR')))
|
389
|
+
money.to_money("EUR").should == Money.new(200_00, "EUR")
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
describe "#exchange_to" do
|
394
|
+
it "exchanges the amount via its exchange bank" do
|
395
|
+
money = Money.new(100_00, "USD")
|
396
|
+
money.bank.should_receive(:exchange_with).with(Money.new(100_00, Money::Currency.new("USD")), Money::Currency.new("EUR")).and_return(Money.new(200_00, Money::Currency.new('EUR')))
|
397
|
+
money.exchange_to("EUR")
|
398
|
+
end
|
399
|
+
|
400
|
+
it "exchanges the amount properly" do
|
401
|
+
money = Money.new(100_00, "USD")
|
402
|
+
money.bank.should_receive(:exchange_with).with(Money.new(100_00, Money::Currency.new("USD")), Money::Currency.new("EUR")).and_return(Money.new(200_00, Money::Currency.new('EUR')))
|
403
|
+
money.exchange_to("EUR").should == Money.new(200_00, "EUR")
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
describe "#allocate" do
|
408
|
+
it "takes no action when one gets all" do
|
409
|
+
Money.us_dollar(005).allocate([1.0]).should == [Money.us_dollar(5)]
|
410
|
+
end
|
411
|
+
|
412
|
+
it "keeps currencies intact" do
|
413
|
+
Money.ca_dollar(005).allocate([1]).should == [Money.ca_dollar(5)]
|
414
|
+
end
|
415
|
+
|
416
|
+
it "does not loose pennies" do
|
417
|
+
moneys = Money.us_dollar(5).allocate([0.3, 0.7])
|
418
|
+
moneys[0].should == Money.us_dollar(2)
|
419
|
+
moneys[1].should == Money.us_dollar(3)
|
420
|
+
end
|
421
|
+
|
422
|
+
it "does not loose pennies" do
|
423
|
+
moneys = Money.us_dollar(100).allocate([0.333, 0.333, 0.333])
|
424
|
+
moneys[0].cents.should == 34
|
425
|
+
moneys[1].cents.should == 33
|
426
|
+
moneys[2].cents.should == 33
|
427
|
+
end
|
428
|
+
|
429
|
+
it "requires total to be less then 1" do
|
430
|
+
expect { Money.us_dollar(0.05).allocate([0.5, 0.6]) }.to raise_error(ArgumentError)
|
431
|
+
end
|
432
|
+
|
433
|
+
context "infinite_precision = true" do
|
434
|
+
before do
|
435
|
+
Money.infinite_precision = true
|
436
|
+
end
|
437
|
+
|
438
|
+
after do
|
439
|
+
Money.infinite_precision = false
|
440
|
+
end
|
441
|
+
|
442
|
+
it "allows for fractional cents allocation" do
|
443
|
+
one_third = BigDecimal("1") / BigDecimal("3")
|
444
|
+
|
445
|
+
moneys = Money.new(100).allocate([one_third, one_third, one_third])
|
446
|
+
moneys[0].cents.should == one_third * BigDecimal("100")
|
447
|
+
moneys[1].cents.should == one_third * BigDecimal("100")
|
448
|
+
moneys[2].cents.should == one_third * BigDecimal("100")
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
describe "#split" do
|
454
|
+
it "needs at least one party" do
|
455
|
+
expect { Money.us_dollar(1).split(0) }.to raise_error(ArgumentError)
|
456
|
+
expect { Money.us_dollar(1).split(-1) }.to raise_error(ArgumentError)
|
457
|
+
end
|
458
|
+
|
459
|
+
it "gives 1 cent to both people if we start with 2" do
|
460
|
+
Money.us_dollar(2).split(2).should == [Money.us_dollar(1), Money.us_dollar(1)]
|
461
|
+
end
|
462
|
+
|
463
|
+
it "may distribute no money to some parties if there isnt enough to go around" do
|
464
|
+
Money.us_dollar(2).split(3).should == [Money.us_dollar(1), Money.us_dollar(1), Money.us_dollar(0)]
|
465
|
+
end
|
466
|
+
|
467
|
+
it "does not lose pennies" do
|
468
|
+
Money.us_dollar(5).split(2).should == [Money.us_dollar(3), Money.us_dollar(2)]
|
469
|
+
end
|
470
|
+
|
471
|
+
it "splits a dollar" do
|
472
|
+
moneys = Money.us_dollar(100).split(3)
|
473
|
+
moneys[0].cents.should == 34
|
474
|
+
moneys[1].cents.should == 33
|
475
|
+
moneys[2].cents.should == 33
|
476
|
+
end
|
477
|
+
|
478
|
+
context "infinite_precision = true" do
|
479
|
+
before do
|
480
|
+
Money.infinite_precision = true
|
481
|
+
end
|
482
|
+
|
483
|
+
after do
|
484
|
+
Money.infinite_precision = false
|
485
|
+
end
|
486
|
+
|
487
|
+
it "allows for splitting by fractional cents" do
|
488
|
+
thirty_three_and_one_third = BigDecimal("100") / BigDecimal("3")
|
489
|
+
|
490
|
+
moneys = Money.new(100).split(3)
|
491
|
+
moneys[0].cents.should == thirty_three_and_one_third
|
492
|
+
moneys[1].cents.should == thirty_three_and_one_third
|
493
|
+
moneys[2].cents.should == thirty_three_and_one_third
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|