money 6.0.1.beta2 → 6.0.1.beta3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/money/bank/variable_exchange.rb +67 -28
- data/lib/money/currency.rb +69 -108
- data/lib/money/currency/heuristics.rb +121 -117
- data/lib/money/currency/loader.rb +19 -17
- data/lib/money/money.rb +81 -85
- data/lib/money/money/arithmetic.rb +42 -31
- data/lib/money/money/formatting.rb +45 -52
- data/lib/money/version.rb +1 -1
- data/spec/bank/variable_exchange_spec.rb +42 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf2817efb2d831c4dc20a21ad35e2668ef18f877
|
4
|
+
data.tar.gz: ab6fb4c372bae304d46f672517844816e2d9788e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f6a4ce82b5f4d9624d9b1568a39357b0d35d190f2c6b94f30bce534f2b2daffa103e40c9da450788c36c2d994e33eeb1554eb1931655c29fa0cd474ceb971402
|
7
|
+
data.tar.gz: f08fda3dc489e7031a6937c28a32678bdef279b8e0e8725c832019576f380da9ee4b217aa6a5d7055985dcdff49cec1d6e48117a0e49843ad5792683290356aa
|
@@ -30,7 +30,7 @@ class Money
|
|
30
30
|
# bank.exchange_with(c2, "USD") #=> #<Money @fractional=803115>
|
31
31
|
class VariableExchange < Base
|
32
32
|
|
33
|
-
attr_reader :rates
|
33
|
+
attr_reader :rates, :mutex
|
34
34
|
|
35
35
|
# Available formats for importing/exporting rates.
|
36
36
|
RATE_FORMATS = [:json, :ruby, :yaml]
|
@@ -84,27 +84,38 @@ class Money
|
|
84
84
|
#
|
85
85
|
# # Exchange 100 CAD to USD:
|
86
86
|
# bank.exchange_with(c2, "USD") #=> #<Money @fractional=803115>
|
87
|
-
def exchange_with(from, to_currency)
|
88
|
-
|
87
|
+
def exchange_with(from, to_currency, &block)
|
88
|
+
to_currency = Currency.wrap(to_currency)
|
89
|
+
if from.currency == to_currency
|
90
|
+
from
|
91
|
+
else
|
92
|
+
if rate = get_rate(from.currency, to_currency)
|
93
|
+
fractional = calculate_fractional(from, to_currency)
|
94
|
+
Money.new(
|
95
|
+
exchange(fractional, rate, &block), to_currency
|
96
|
+
)
|
97
|
+
else
|
98
|
+
raise UnknownRate, "No conversion rate known for '#{from.currency.iso_code}' -> '#{to_currency}'"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def calculate_fractional(from, to_currency)
|
104
|
+
BigDecimal.new(from.fractional.to_s) / (
|
105
|
+
BigDecimal.new(from.currency.subunit_to_unit.to_s) /
|
106
|
+
BigDecimal.new(to_currency.subunit_to_unit.to_s)
|
107
|
+
)
|
108
|
+
end
|
89
109
|
|
90
|
-
|
91
|
-
|
92
|
-
|
110
|
+
def exchange(fractional, rate, &block)
|
111
|
+
ex = (fractional * BigDecimal.new(rate.to_s)).to_f
|
112
|
+
if block_given?
|
113
|
+
yield ex
|
114
|
+
elsif @rounding_method
|
115
|
+
@rounding_method.call(ex)
|
116
|
+
else
|
117
|
+
ex.to_s.to_i
|
93
118
|
end
|
94
|
-
_to_currency_ = Currency.wrap(to_currency)
|
95
|
-
|
96
|
-
fractional = BigDecimal.new(from.fractional.to_s) / (BigDecimal.new(from.currency.subunit_to_unit.to_s) / BigDecimal.new(_to_currency_.subunit_to_unit.to_s))
|
97
|
-
|
98
|
-
ex = fractional * BigDecimal.new(rate.to_s)
|
99
|
-
ex = ex.to_f
|
100
|
-
ex = if block_given?
|
101
|
-
yield ex
|
102
|
-
elsif @rounding_method
|
103
|
-
@rounding_method.call(ex)
|
104
|
-
else
|
105
|
-
ex.to_s.to_i
|
106
|
-
end
|
107
|
-
Money.new(ex, _to_currency_)
|
108
119
|
end
|
109
120
|
|
110
121
|
# Registers a conversion rate and returns it (uses +#set_rate+).
|
@@ -129,6 +140,8 @@ class Money
|
|
129
140
|
# @param [Currency, String, Symbol] from Currency to exchange from.
|
130
141
|
# @param [Currency, String, Symbol] to Currency to exchange to.
|
131
142
|
# @param [Numeric] rate Rate to use when exchanging currencies.
|
143
|
+
# @param [Hash] opts Options hash to set special parameters
|
144
|
+
# @option opts [Boolean] :without_mutex disables the usage of a mutex
|
132
145
|
#
|
133
146
|
# @return [Numeric]
|
134
147
|
#
|
@@ -136,8 +149,13 @@ class Money
|
|
136
149
|
# bank = Money::Bank::VariableExchange.new
|
137
150
|
# bank.set_rate("USD", "CAD", 1.24515)
|
138
151
|
# bank.set_rate("CAD", "USD", 0.803115)
|
139
|
-
def set_rate(from, to, rate)
|
140
|
-
|
152
|
+
def set_rate(from, to, rate, opts = {})
|
153
|
+
fn = -> { @rates[rate_key_for(from, to)] = rate }
|
154
|
+
if opts[:without_mutex]
|
155
|
+
fn.call
|
156
|
+
else
|
157
|
+
@mutex.synchronize { fn.call }
|
158
|
+
end
|
141
159
|
end
|
142
160
|
|
143
161
|
# Retrieve the rate for the given currencies. Uses +Mutex+ to synchronize
|
@@ -145,6 +163,8 @@ class Money
|
|
145
163
|
#
|
146
164
|
# @param [Currency, String, Symbol] from Currency to exchange from.
|
147
165
|
# @param [Currency, String, Symbol] to Currency to exchange to.
|
166
|
+
# @param [Hash] opts Options hash to set special parameters
|
167
|
+
# @option opts [Boolean] :without_mutex disables the usage of a mutex
|
148
168
|
#
|
149
169
|
# @return [Numeric]
|
150
170
|
#
|
@@ -155,8 +175,13 @@ class Money
|
|
155
175
|
#
|
156
176
|
# bank.get_rate("USD", "CAD") #=> 1.24515
|
157
177
|
# bank.get_rate("CAD", "USD") #=> 0.803115
|
158
|
-
def get_rate(from, to)
|
159
|
-
|
178
|
+
def get_rate(from, to, opts = {})
|
179
|
+
fn = -> { @rates[rate_key_for(from, to)] }
|
180
|
+
if opts[:without_mutex]
|
181
|
+
fn.call
|
182
|
+
else
|
183
|
+
@mutex.synchronize { fn.call }
|
184
|
+
end
|
160
185
|
end
|
161
186
|
|
162
187
|
# Return the known rates as a string in the format specified. If +file+
|
@@ -165,6 +190,8 @@ class Money
|
|
165
190
|
#
|
166
191
|
# @param [Symbol] format Request format for the resulting string.
|
167
192
|
# @param [String] file Optional file location to write the rates to.
|
193
|
+
# @param [Hash] opts Options hash to set special parameters
|
194
|
+
# @option opts [Boolean] :without_mutex disables the usage of a mutex
|
168
195
|
#
|
169
196
|
# @return [String]
|
170
197
|
#
|
@@ -177,12 +204,12 @@ class Money
|
|
177
204
|
#
|
178
205
|
# s = bank.export_rates(:json)
|
179
206
|
# s #=> "{\"USD_TO_CAD\":1.24515,\"CAD_TO_USD\":0.803115}"
|
180
|
-
def export_rates(format, file=nil)
|
207
|
+
def export_rates(format, file = nil, opts = {})
|
181
208
|
raise Money::Bank::UnknownRateFormat unless
|
182
209
|
RATE_FORMATS.include? format
|
183
210
|
|
184
211
|
s = ""
|
185
|
-
|
212
|
+
fn = -> {
|
186
213
|
s = case format
|
187
214
|
when :json
|
188
215
|
JSON.dump(@rates)
|
@@ -196,6 +223,11 @@ class Money
|
|
196
223
|
File.open(file, "w") {|f| f.write(s) }
|
197
224
|
end
|
198
225
|
}
|
226
|
+
if opts[:without_mutex]
|
227
|
+
fn.call
|
228
|
+
else
|
229
|
+
@mutex.synchronize { fn.call }
|
230
|
+
end
|
199
231
|
s
|
200
232
|
end
|
201
233
|
|
@@ -204,6 +236,8 @@ class Money
|
|
204
236
|
#
|
205
237
|
# @param [Symbol] format The format of +s+.
|
206
238
|
# @param [String] s The rates string.
|
239
|
+
# @param [Hash] opts Options hash to set special parameters
|
240
|
+
# @option opts [Boolean] :without_mutex disables the usage of a mutex
|
207
241
|
#
|
208
242
|
# @return [self]
|
209
243
|
#
|
@@ -216,11 +250,11 @@ class Money
|
|
216
250
|
#
|
217
251
|
# bank.get_rate("USD", "CAD") #=> 1.24515
|
218
252
|
# bank.get_rate("CAD", "USD") #=> 0.803115
|
219
|
-
def import_rates(format, s)
|
253
|
+
def import_rates(format, s, opts = {})
|
220
254
|
raise Money::Bank::UnknownRateFormat unless
|
221
255
|
RATE_FORMATS.include? format
|
222
256
|
|
223
|
-
|
257
|
+
fn = -> {
|
224
258
|
@rates = case format
|
225
259
|
when :json
|
226
260
|
JSON.load(s)
|
@@ -230,6 +264,11 @@ class Money
|
|
230
264
|
YAML.load(s)
|
231
265
|
end
|
232
266
|
}
|
267
|
+
if opts[:without_mutex]
|
268
|
+
fn.call
|
269
|
+
else
|
270
|
+
@mutex.synchronize { fn.call }
|
271
|
+
end
|
233
272
|
self
|
234
273
|
end
|
235
274
|
|
data/lib/money/currency.rb
CHANGED
@@ -1,20 +1,19 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require
|
3
|
+
require "json"
|
4
|
+
require "money/currency/loader"
|
5
|
+
require "money/currency/heuristics"
|
4
6
|
|
5
7
|
class Money
|
6
8
|
|
7
9
|
# Represents a specific currency unit.
|
8
10
|
#
|
9
11
|
# @see http://en.wikipedia.org/wiki/Currency
|
12
|
+
# @see http://iso4217.net/
|
10
13
|
class Currency
|
11
14
|
include Comparable
|
12
|
-
|
13
|
-
|
14
|
-
extend Loader
|
15
|
-
|
16
|
-
require "money/currency/heuristics"
|
17
|
-
extend Heuristics
|
15
|
+
extend Money::Currency::Loader
|
16
|
+
extend Money::Currency::Heuristics
|
18
17
|
|
19
18
|
# Thrown when an unknown currency is requested.
|
20
19
|
class UnknownCurrency < StandardError; end
|
@@ -34,7 +33,9 @@ class Money
|
|
34
33
|
# Money::Currency.find(:foo) #=> nil
|
35
34
|
def find(id)
|
36
35
|
id = id.to_s.downcase.to_sym
|
37
|
-
new(id)
|
36
|
+
new(id)
|
37
|
+
rescue UnknownCurrency
|
38
|
+
nil
|
38
39
|
end
|
39
40
|
|
40
41
|
# Lookup a currency with given +num+ as an ISO 4217 numeric and returns an
|
@@ -51,7 +52,9 @@ class Money
|
|
51
52
|
def find_by_iso_numeric(num)
|
52
53
|
num = num.to_s
|
53
54
|
id, _ = self.table.find{|key, currency| currency[:iso_numeric] == num}
|
54
|
-
new(id)
|
55
|
+
new(id)
|
56
|
+
rescue UnknownCurrency
|
57
|
+
nil
|
55
58
|
end
|
56
59
|
|
57
60
|
# Wraps the object in a +Currency+ unless it's already a +Currency+
|
@@ -68,9 +71,7 @@ class Money
|
|
68
71
|
# Money::Currency.wrap(c1) #=> #<Money::Currency id: usd ...>
|
69
72
|
# Money::Currency.wrap("usd") #=> #<Money::Currency id: usd ...>
|
70
73
|
def wrap(object)
|
71
|
-
if object.nil?
|
72
|
-
nil
|
73
|
-
elsif object.is_a?(Currency)
|
74
|
+
if object.nil? || object.is_a?(Currency)
|
74
75
|
object
|
75
76
|
else
|
76
77
|
Currency.new(object)
|
@@ -128,71 +129,33 @@ class Money
|
|
128
129
|
end
|
129
130
|
end
|
130
131
|
|
131
|
-
# The symbol used to identify the currency,
|
132
|
-
# +iso_code+ attribute.
|
133
|
-
#
|
134
|
-
#
|
135
|
-
attr_reader
|
136
|
-
|
137
|
-
#
|
138
|
-
#
|
139
|
-
# @
|
140
|
-
attr_reader
|
141
|
-
|
142
|
-
#
|
143
|
-
#
|
144
|
-
#
|
145
|
-
# @
|
146
|
-
|
147
|
-
|
148
|
-
#
|
149
|
-
#
|
150
|
-
#
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
attr_reader :name
|
159
|
-
|
160
|
-
# The currency symbol (UTF-8 encoded).
|
161
|
-
#
|
162
|
-
# @return [String]
|
163
|
-
attr_reader :symbol
|
164
|
-
|
165
|
-
# The html entity for the currency symbol
|
166
|
-
#
|
167
|
-
# @return [String]
|
168
|
-
attr_reader :html_entity
|
169
|
-
|
170
|
-
# The name of the fractional monetary unit.
|
171
|
-
#
|
172
|
-
# @return [String]
|
173
|
-
attr_reader :subunit
|
174
|
-
|
175
|
-
# The proportion between the unit and the subunit
|
176
|
-
#
|
177
|
-
# @return [Integer]
|
178
|
-
attr_reader :subunit_to_unit
|
179
|
-
|
180
|
-
# The decimal mark, or character used to separate the whole unit from the subunit.
|
181
|
-
#
|
182
|
-
# @return [String]
|
183
|
-
attr_reader :decimal_mark
|
184
|
-
alias :separator :decimal_mark
|
185
|
-
|
186
|
-
# The character used to separate thousands grouping of the whole unit.
|
187
|
-
#
|
188
|
-
# @return [String]
|
189
|
-
attr_reader :thousands_separator
|
190
|
-
alias :delimiter :thousands_separator
|
191
|
-
|
192
|
-
# Should the currency symbol precede the amount, or should it come after?
|
193
|
-
#
|
194
|
-
# @return [Boolean]
|
195
|
-
attr_reader :symbol_first
|
132
|
+
# @attr_reader [Symbol] id The symbol used to identify the currency,
|
133
|
+
# usually the lowercase +iso_code+ attribute.
|
134
|
+
# @attr_reader [Integer] priority A numerical value you can use to
|
135
|
+
# sort/group the currency list.
|
136
|
+
# @attr_reader [String] iso_code The international 3-letter code as defined
|
137
|
+
# by the ISO 4217 standard.
|
138
|
+
# @attr_reader [String] iso_numeric The international 3-numeric code as
|
139
|
+
# defined by the ISO 4217 standard.
|
140
|
+
# @attr_reader [String] name The currency name.
|
141
|
+
# @attr_reader [String] symbol The currency symbol (UTF-8 encoded).
|
142
|
+
# @attr_reader [String] html_entity The html entity for the currency symbol
|
143
|
+
# @attr_reader [String] subunit The name of the fractional monetary unit.
|
144
|
+
# @attr_reader [Integer] subunit_to_unit The proportion between the unit
|
145
|
+
# and the subunit
|
146
|
+
# @attr_reader [String] decimal_mark The decimal mark, or character used to
|
147
|
+
# separate the whole unit from the subunit.
|
148
|
+
# @attr_reader [String] The character used to separate thousands grouping
|
149
|
+
# of the whole unit.
|
150
|
+
# @attr_reader [Boolean] symbol_first Should the currency symbol precede
|
151
|
+
# the amount, or should it come after?
|
152
|
+
|
153
|
+
attr_reader :id, :priority, :iso_code, :iso_numeric, :name, :symbol,
|
154
|
+
:html_entity, :subunit, :subunit_to_unit, :decimal_mark,
|
155
|
+
:thousands_separator, :symbol_first
|
156
|
+
|
157
|
+
alias_method :separator, :decimal_mark
|
158
|
+
alias_method :delimiter, :thousands_separator
|
196
159
|
|
197
160
|
# Create a new +Currency+ object.
|
198
161
|
#
|
@@ -205,12 +168,15 @@ class Money
|
|
205
168
|
# Money::Currency.new(:usd) #=> #<Money::Currency id: usd ...>
|
206
169
|
def initialize(id)
|
207
170
|
id = id.to_s.downcase
|
208
|
-
raise(UnknownCurrency, "Unknown currency `#{id}'") unless self.class.stringified_keys.include?(id)
|
209
171
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
172
|
+
if self.class.stringified_keys.include?(id)
|
173
|
+
@id = id.to_sym
|
174
|
+
data = self.class.table[@id]
|
175
|
+
data.each_pair do |key, value|
|
176
|
+
instance_variable_set(:"@#{key}", value)
|
177
|
+
end
|
178
|
+
else
|
179
|
+
raise UnknownCurrency, "Unknown currency '#{id}'"
|
214
180
|
end
|
215
181
|
end
|
216
182
|
|
@@ -244,9 +210,18 @@ class Money
|
|
244
210
|
# c1 == c1 #=> true
|
245
211
|
# c1 == c2 #=> false
|
246
212
|
def ==(other_currency)
|
247
|
-
self.equal?(other_currency) ||
|
248
|
-
|
213
|
+
self.equal?(other_currency) || compare_ids(other_currency)
|
214
|
+
end
|
215
|
+
|
216
|
+
def compare_ids(other_currency)
|
217
|
+
other_currency_id = if other_currency.is_a?(Currency)
|
218
|
+
other_currency.id.to_s.downcase
|
219
|
+
else
|
220
|
+
other_currency.to_s.downcase
|
221
|
+
end
|
222
|
+
self.id.to_s.downcase == other_currency_id
|
249
223
|
end
|
224
|
+
private :compare_ids
|
250
225
|
|
251
226
|
# Compares +self+ with +other_currency+ and returns +true+ if the are the
|
252
227
|
# same or if their +id+ attributes match.
|
@@ -288,7 +263,7 @@ class Money
|
|
288
263
|
# Returns a string representation corresponding to the upcase +id+
|
289
264
|
# attribute.
|
290
265
|
#
|
291
|
-
#
|
266
|
+
# --
|
292
267
|
# DEV: id.to_s.upcase corresponds to iso_code but don't use ISO_CODE for consistency.
|
293
268
|
#
|
294
269
|
# @return [String]
|
@@ -311,7 +286,7 @@ class Money
|
|
311
286
|
def to_str
|
312
287
|
id.to_s.upcase
|
313
288
|
end
|
314
|
-
|
289
|
+
|
315
290
|
# Returns a symbol representation corresponding to the upcase +id+
|
316
291
|
# attribute.
|
317
292
|
#
|
@@ -321,12 +296,7 @@ class Money
|
|
321
296
|
# Money::Currency.new(:usd).to_sym #=> :USD
|
322
297
|
# Money::Currency.new(:eur).to_sym #=> :EUR
|
323
298
|
def to_sym
|
324
|
-
|
325
|
-
id.upcase
|
326
|
-
else
|
327
|
-
# Ruby <= 1.8.7 doesn't support Symbol#upcase
|
328
|
-
id.to_s.upcase.to_sym
|
329
|
-
end
|
299
|
+
id.to_s.upcase.to_sym
|
330
300
|
end
|
331
301
|
|
332
302
|
# Conversation to +self+.
|
@@ -336,7 +306,6 @@ class Money
|
|
336
306
|
self
|
337
307
|
end
|
338
308
|
|
339
|
-
|
340
309
|
# Returns currency symbol or iso code for currencies with no symbol.
|
341
310
|
#
|
342
311
|
# @return [String]
|
@@ -357,31 +326,24 @@ class Money
|
|
357
326
|
|
358
327
|
# Cache decimal places for subunit_to_unit values. Common ones pre-cached.
|
359
328
|
def self.decimal_places_cache
|
360
|
-
@decimal_places_cache ||= {
|
361
|
-
1 => 0,
|
362
|
-
10 => 1,
|
363
|
-
100 => 2,
|
364
|
-
1000 => 3
|
365
|
-
}
|
329
|
+
@decimal_places_cache ||= {1 => 0, 10 => 1, 100 => 2, 1000 => 3}
|
366
330
|
end
|
367
331
|
|
368
332
|
# The number of decimal places needed.
|
369
333
|
#
|
370
334
|
# @return [Integer]
|
371
335
|
def decimal_places
|
372
|
-
cache
|
373
|
-
places = cache[subunit_to_unit]
|
374
|
-
unless places
|
375
|
-
places = calculate_decimal_places(subunit_to_unit)
|
376
|
-
cache[subunit_to_unit] = places
|
377
|
-
end
|
378
|
-
places
|
336
|
+
cache[subunit_to_unit] ||= calculate_decimal_places(subunit_to_unit)
|
379
337
|
end
|
380
338
|
|
339
|
+
def cache
|
340
|
+
self.class.decimal_places_cache
|
341
|
+
end
|
342
|
+
private :cache
|
343
|
+
|
381
344
|
# If we need to figure out how many decimal places we need we
|
382
345
|
# use repeated integer division.
|
383
346
|
def calculate_decimal_places(num)
|
384
|
-
return 0 if num == 1
|
385
347
|
i = 1
|
386
348
|
while num >= 10
|
387
349
|
num /= 10
|
@@ -390,6 +352,5 @@ class Money
|
|
390
352
|
i
|
391
353
|
end
|
392
354
|
private :calculate_decimal_places
|
393
|
-
|
394
355
|
end
|
395
356
|
end
|