money-joshm1 5.1.2
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.
- 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
|