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,355 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
class Money
|
6
|
+
|
7
|
+
# Represents a specific currency unit.
|
8
|
+
class Currency
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
require "money/currency/loader"
|
12
|
+
extend Loader
|
13
|
+
|
14
|
+
require "money/currency/heuristics"
|
15
|
+
extend Heuristics
|
16
|
+
|
17
|
+
# Thrown when an unknown currency is requested.
|
18
|
+
class UnknownCurrency < StandardError; end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
|
22
|
+
# Lookup a currency with given +id+ an returns a +Currency+ instance on
|
23
|
+
# success, +nil+ otherwise.
|
24
|
+
#
|
25
|
+
# @param [String, Symbol, #to_s] id Used to look into +table+ and
|
26
|
+
# retrieve the applicable attributes.
|
27
|
+
#
|
28
|
+
# @return [Money::Currency]
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# Money::Currency.find(:eur) #=> #<Money::Currency id: eur ...>
|
32
|
+
# Money::Currency.find(:foo) #=> nil
|
33
|
+
def find(id)
|
34
|
+
id = id.to_s.downcase.to_sym
|
35
|
+
new(id) if self.table[id]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Lookup a currency with given +num+ as an ISO 4217 numeric and returns an
|
39
|
+
# +Currency+ instance on success, +nil+ otherwise.
|
40
|
+
#
|
41
|
+
# @param [#to_s] num used to look into +table+ in +iso_numeric+ and find
|
42
|
+
# the right currency id.
|
43
|
+
#
|
44
|
+
# @return [Money::Currency]
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# Money::Currency.find_by_iso_numeric(978) #=> #<Money::Currency id: eur ...>
|
48
|
+
# Money::Currency.find_by_iso_numeric('001') #=> nil
|
49
|
+
def find_by_iso_numeric(num)
|
50
|
+
num = num.to_s
|
51
|
+
id, _ = self.table.find{|key, currency| currency[:iso_numeric] == num}
|
52
|
+
new(id) if self.table[id]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Wraps the object in a +Currency+ unless it's already a +Currency+
|
56
|
+
# object.
|
57
|
+
#
|
58
|
+
# @param [Object] object The object to attempt and wrap as a +Currency+
|
59
|
+
# object.
|
60
|
+
#
|
61
|
+
# @return [Money::Currency]
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# c1 = Money::Currency.new(:usd)
|
65
|
+
# Money::Currency.wrap(nil) #=> nil
|
66
|
+
# Money::Currency.wrap(c1) #=> #<Money::Currency id: usd ...>
|
67
|
+
# Money::Currency.wrap("usd") #=> #<Money::Currency id: usd ...>
|
68
|
+
def wrap(object)
|
69
|
+
if object.nil?
|
70
|
+
nil
|
71
|
+
elsif object.is_a?(Currency)
|
72
|
+
object
|
73
|
+
else
|
74
|
+
Currency.new(object)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# List of known currencies.
|
79
|
+
#
|
80
|
+
# == monetary unit
|
81
|
+
# The standard unit of value of a currency, as the dollar in the United States or the peso in Mexico.
|
82
|
+
# http://www.answers.com/topic/monetary-unit
|
83
|
+
# == fractional monetary unit, subunit
|
84
|
+
# A monetary unit that is valued at a fraction (usually one hundredth) of the basic monetary unit
|
85
|
+
# http://www.answers.com/topic/fractional-monetary-unit-subunit
|
86
|
+
#
|
87
|
+
# See http://en.wikipedia.org/wiki/List_of_circulating_currencies and
|
88
|
+
# http://search.cpan.org/~tnguyen/Locale-Currency-Format-1.28/Format.pm
|
89
|
+
def table
|
90
|
+
@table ||= load_currencies
|
91
|
+
end
|
92
|
+
|
93
|
+
# List the currencies imported and registered
|
94
|
+
# @return [Array]
|
95
|
+
#
|
96
|
+
# @example
|
97
|
+
# Money::Currency.iso_codes()
|
98
|
+
# [#<Currency ..USD>, 'CAD', 'EUR']...
|
99
|
+
def all
|
100
|
+
table.keys.map {|curr| Currency.new(curr)}.sort_by(&:priority)
|
101
|
+
end
|
102
|
+
|
103
|
+
# We need a string-based validator before creating an unbounded number of symbols.
|
104
|
+
# http://www.randomhacks.net/articles/2007/01/20/13-ways-of-looking-at-a-ruby-symbol#11
|
105
|
+
# https://github.com/RubyMoney/money/issues/132
|
106
|
+
def stringified_keys
|
107
|
+
@stringified_keys ||= stringify_keys
|
108
|
+
end
|
109
|
+
|
110
|
+
def register(curr)
|
111
|
+
key = curr[:iso_code].downcase.to_sym
|
112
|
+
@table[key] = curr
|
113
|
+
@stringified_keys = stringify_keys
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def stringify_keys
|
119
|
+
table.keys.map{|k| k.to_s.downcase}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# The symbol used to identify the currency, usually the lowercase
|
124
|
+
# +iso_code+ attribute.
|
125
|
+
#
|
126
|
+
# @return [Symbol]
|
127
|
+
attr_reader :id
|
128
|
+
|
129
|
+
# A numerical value you can use to sort/group the currency list.
|
130
|
+
#
|
131
|
+
# @return [Integer]
|
132
|
+
attr_reader :priority
|
133
|
+
|
134
|
+
# The international 3-letter code as defined by the ISO 4217 standard.
|
135
|
+
#
|
136
|
+
# @return [String]
|
137
|
+
attr_reader :iso_code
|
138
|
+
#
|
139
|
+
# The international 3-numeric code as defined by the ISO 4217 standard.
|
140
|
+
#
|
141
|
+
# @return [String]
|
142
|
+
attr_reader :iso_numeric
|
143
|
+
|
144
|
+
# The currency name.
|
145
|
+
#
|
146
|
+
# @return [String]
|
147
|
+
attr_reader :name
|
148
|
+
|
149
|
+
# The currency symbol (UTF-8 encoded).
|
150
|
+
#
|
151
|
+
# @return [String]
|
152
|
+
attr_reader :symbol
|
153
|
+
|
154
|
+
# The html entity for the currency symbol
|
155
|
+
#
|
156
|
+
# @return [String]
|
157
|
+
attr_reader :html_entity
|
158
|
+
|
159
|
+
# The name of the fractional monetary unit.
|
160
|
+
#
|
161
|
+
# @return [String]
|
162
|
+
attr_reader :subunit
|
163
|
+
|
164
|
+
# The proportion between the unit and the subunit
|
165
|
+
#
|
166
|
+
# @return [Integer]
|
167
|
+
attr_reader :subunit_to_unit
|
168
|
+
|
169
|
+
# The decimal mark, or character used to separate the whole unit from the subunit.
|
170
|
+
#
|
171
|
+
# @return [String]
|
172
|
+
attr_reader :decimal_mark
|
173
|
+
alias :separator :decimal_mark
|
174
|
+
|
175
|
+
# The character used to separate thousands grouping of the whole unit.
|
176
|
+
#
|
177
|
+
# @return [String]
|
178
|
+
attr_reader :thousands_separator
|
179
|
+
alias :delimiter :thousands_separator
|
180
|
+
|
181
|
+
# Should the currency symbol precede the amount, or should it come after?
|
182
|
+
#
|
183
|
+
# @return [Boolean]
|
184
|
+
attr_reader :symbol_first
|
185
|
+
|
186
|
+
# Create a new +Currency+ object.
|
187
|
+
#
|
188
|
+
# @param [String, Symbol, #to_s] id Used to look into +table+ and retrieve
|
189
|
+
# the applicable attributes.
|
190
|
+
#
|
191
|
+
# @return [Money::Currency]
|
192
|
+
#
|
193
|
+
# @example
|
194
|
+
# Money::Currency.new(:usd) #=> #<Money::Currency id: usd ...>
|
195
|
+
def initialize(id)
|
196
|
+
id = id.to_s.downcase
|
197
|
+
raise(UnknownCurrency, "Unknown currency `#{id}'") unless self.class.stringified_keys.include?(id)
|
198
|
+
|
199
|
+
@id = id.to_sym
|
200
|
+
data = self.class.table[@id]
|
201
|
+
data.each_pair do |key, value|
|
202
|
+
instance_variable_set(:"@#{key}", value)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Compares +self+ with +other_currency+ against the value of +priority+
|
207
|
+
# attribute.
|
208
|
+
#
|
209
|
+
# @param [Money::Currency] other_currency The currency to compare to.
|
210
|
+
#
|
211
|
+
# @return [-1,0,1] -1 if less than, 0 is equal to, 1 if greater than
|
212
|
+
#
|
213
|
+
# @example
|
214
|
+
# c1 = Money::Currency.new(:usd)
|
215
|
+
# c2 = Money::Currency.new(:jpy)
|
216
|
+
# c1 <=> c2 #=> 1
|
217
|
+
# c2 <=> c1 #=> -1
|
218
|
+
# c1 <=> c1 #=> 0
|
219
|
+
def <=>(other_currency)
|
220
|
+
self.priority <=> other_currency.priority
|
221
|
+
end
|
222
|
+
|
223
|
+
# Compares +self+ with +other_currency+ and returns +true+ if the are the
|
224
|
+
# same or if their +id+ attributes match.
|
225
|
+
#
|
226
|
+
# @param [Money::Currency] other_currency The currency to compare to.
|
227
|
+
#
|
228
|
+
# @return [Boolean]
|
229
|
+
#
|
230
|
+
# @example
|
231
|
+
# c1 = Money::Currency.new(:usd)
|
232
|
+
# c2 = Money::Currency.new(:jpy)
|
233
|
+
# c1 == c1 #=> true
|
234
|
+
# c1 == c2 #=> false
|
235
|
+
def ==(other_currency)
|
236
|
+
self.equal?(other_currency) ||
|
237
|
+
self.id.to_s.downcase == (other_currency.is_a?(Currency) ? other_currency.id.to_s.downcase : other_currency.to_s.downcase)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Compares +self+ with +other_currency+ and returns +true+ if the are the
|
241
|
+
# same or if their +id+ attributes match.
|
242
|
+
#
|
243
|
+
# @param [Money::Currency] other_currency The currency to compare to.
|
244
|
+
#
|
245
|
+
# @return [Boolean]
|
246
|
+
#
|
247
|
+
# @example
|
248
|
+
# c1 = Money::Currency.new(:usd)
|
249
|
+
# c2 = Money::Currency.new(:jpy)
|
250
|
+
# c1.eql? c1 #=> true
|
251
|
+
# c1.eql? c2 #=> false
|
252
|
+
def eql?(other_currency)
|
253
|
+
self == other_currency
|
254
|
+
end
|
255
|
+
|
256
|
+
# Returns a Fixnum hash value based on the +id+ attribute in order to use
|
257
|
+
# functions like & (intersection), group_by, etc.
|
258
|
+
#
|
259
|
+
# @return [Fixnum]
|
260
|
+
#
|
261
|
+
# @example
|
262
|
+
# Money::Currency.new(:usd).hash #=> 428936
|
263
|
+
def hash
|
264
|
+
id.hash
|
265
|
+
end
|
266
|
+
|
267
|
+
# Returns a human readable representation.
|
268
|
+
#
|
269
|
+
# @return [String]
|
270
|
+
#
|
271
|
+
# @example
|
272
|
+
# Money::Currency.new(:usd) #=> #<Currency id: usd ...>
|
273
|
+
def inspect
|
274
|
+
"#<#{self.class.name} id: #{id}, priority: #{priority}, symbol_first: #{symbol_first}, thousands_separator: #{thousands_separator}, html_entity: #{html_entity}, decimal_mark: #{decimal_mark}, name: #{name}, symbol: #{symbol}, subunit_to_unit: #{subunit_to_unit}, exponent: #{exponent}, iso_code: #{iso_code}, iso_numeric: #{iso_numeric}, subunit: #{subunit}>"
|
275
|
+
end
|
276
|
+
|
277
|
+
# Returns a string representation corresponding to the upcase +id+
|
278
|
+
# attribute.
|
279
|
+
#
|
280
|
+
# -–
|
281
|
+
# DEV: id.to_s.upcase corresponds to iso_code but don't use ISO_CODE for consistency.
|
282
|
+
#
|
283
|
+
# @return [String]
|
284
|
+
#
|
285
|
+
# @example
|
286
|
+
# Money::Currency.new(:usd).to_s #=> "USD"
|
287
|
+
# Money::Currency.new(:eur).to_s #=> "EUR"
|
288
|
+
def to_s
|
289
|
+
id.to_s.upcase
|
290
|
+
end
|
291
|
+
|
292
|
+
# Conversation to +self+.
|
293
|
+
#
|
294
|
+
# @return [self]
|
295
|
+
def to_currency
|
296
|
+
self
|
297
|
+
end
|
298
|
+
|
299
|
+
|
300
|
+
# Returns currency symbol or iso code for currencies with no symbol.
|
301
|
+
#
|
302
|
+
# @return [String]
|
303
|
+
def code
|
304
|
+
symbol || iso_code
|
305
|
+
end
|
306
|
+
|
307
|
+
def symbol_first?
|
308
|
+
!!@symbol_first
|
309
|
+
end
|
310
|
+
|
311
|
+
# Returns the number of digits after the decimal separator.
|
312
|
+
#
|
313
|
+
# @return [Float]
|
314
|
+
def exponent
|
315
|
+
Math.log10(@subunit_to_unit)
|
316
|
+
end
|
317
|
+
|
318
|
+
# Cache decimal places for subunit_to_unit values. Common ones pre-cached.
|
319
|
+
def self.decimal_places_cache
|
320
|
+
@decimal_places_cache ||= {
|
321
|
+
1 => 0,
|
322
|
+
10 => 1,
|
323
|
+
100 => 2,
|
324
|
+
1000 => 3
|
325
|
+
}
|
326
|
+
end
|
327
|
+
|
328
|
+
# The number of decimal places needed.
|
329
|
+
#
|
330
|
+
# @return [Integer]
|
331
|
+
def decimal_places
|
332
|
+
cache = self.class.decimal_places_cache
|
333
|
+
places = cache[subunit_to_unit]
|
334
|
+
unless places
|
335
|
+
places = calculate_decimal_places(subunit_to_unit)
|
336
|
+
cache[subunit_to_unit] = places
|
337
|
+
end
|
338
|
+
places
|
339
|
+
end
|
340
|
+
|
341
|
+
# If we need to figure out how many decimal places we need we
|
342
|
+
# use repeated integer division.
|
343
|
+
def calculate_decimal_places(num)
|
344
|
+
return 0 if num == 1
|
345
|
+
i = 1
|
346
|
+
while num >= 10
|
347
|
+
num /= 10
|
348
|
+
i += 1 if num >= 10
|
349
|
+
end
|
350
|
+
i
|
351
|
+
end
|
352
|
+
private :calculate_decimal_places
|
353
|
+
|
354
|
+
end
|
355
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Money::Currency::Heuristics
|
4
|
+
|
5
|
+
# An robust and efficient algorithm for finding currencies in
|
6
|
+
# text. Using several algorithms it can find symbols, iso codes and
|
7
|
+
# even names of currencies.
|
8
|
+
# Although not recommendable, it can also attempt to find the given
|
9
|
+
# currency in an entire sentence
|
10
|
+
#
|
11
|
+
# Returns: Array (matched results)
|
12
|
+
def analyze(str)
|
13
|
+
return Analyzer.new(str, search_tree).process
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Build a search tree from the currency database
|
19
|
+
def search_tree
|
20
|
+
@_search_tree ||= {
|
21
|
+
:by_symbol => currencies_by_symbol,
|
22
|
+
:by_iso_code => currencies_by_iso_code,
|
23
|
+
:by_name => currencies_by_name
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def currencies_by_symbol
|
28
|
+
{}.tap do |r|
|
29
|
+
table.each do |dummy, c|
|
30
|
+
symbol = (c[:symbol]||"").downcase
|
31
|
+
symbol.chomp!('.')
|
32
|
+
(r[symbol] ||= []) << c
|
33
|
+
|
34
|
+
(c[:alternate_symbols]||[]).each do |ac|
|
35
|
+
ac = ac.downcase
|
36
|
+
ac.chomp!('.')
|
37
|
+
(r[ac] ||= []) << c
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def currencies_by_iso_code
|
44
|
+
{}.tap do |r|
|
45
|
+
table.each do |dummy,c|
|
46
|
+
(r[c[:iso_code].downcase] ||= []) << c
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def currencies_by_name
|
52
|
+
{}.tap do |r|
|
53
|
+
table.each do |dummy,c|
|
54
|
+
name_parts = c[:name].downcase.split
|
55
|
+
name_parts.each {|part| part.chomp!('.')}
|
56
|
+
|
57
|
+
# construct one branch per word
|
58
|
+
root = r
|
59
|
+
while name_part = name_parts.shift
|
60
|
+
root = (root[name_part] ||= {})
|
61
|
+
end
|
62
|
+
|
63
|
+
# the leaf is a currency
|
64
|
+
(root[:value] ||= []) << c
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Analyzer
|
70
|
+
attr_reader :search_tree, :words
|
71
|
+
attr_accessor :str, :currencies
|
72
|
+
|
73
|
+
def initialize str, search_tree
|
74
|
+
@str = (str||'').dup
|
75
|
+
@search_tree = search_tree
|
76
|
+
@currencies = []
|
77
|
+
end
|
78
|
+
|
79
|
+
def process
|
80
|
+
format
|
81
|
+
return [] if str.empty?
|
82
|
+
|
83
|
+
search_by_symbol
|
84
|
+
search_by_iso_code
|
85
|
+
search_by_name
|
86
|
+
|
87
|
+
prepare_reply
|
88
|
+
end
|
89
|
+
|
90
|
+
def format
|
91
|
+
str.gsub!(/[\r\n\t]/,'')
|
92
|
+
str.gsub!(/[0-9][\.,:0-9]*[0-9]/,'')
|
93
|
+
str.gsub!(/[0-9]/, '')
|
94
|
+
str.downcase!
|
95
|
+
@words = str.split
|
96
|
+
@words.each {|word| word.chomp!('.'); word.chomp!(',') }
|
97
|
+
end
|
98
|
+
|
99
|
+
def search_by_symbol
|
100
|
+
words.each do |word|
|
101
|
+
if found = search_tree[:by_symbol][word]
|
102
|
+
currencies.concat(found)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def search_by_iso_code
|
108
|
+
words.each do |word|
|
109
|
+
if found = search_tree[:by_iso_code][word]
|
110
|
+
currencies.concat(found)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def search_by_name
|
116
|
+
# remember, the search tree by name is a construct of branches and leaf!
|
117
|
+
# We need to try every combination of words within the sentence, so we
|
118
|
+
# end up with a x^2 equation, which should be fine as most names are either
|
119
|
+
# one or two words, and this is multiplied with the words of given sentence
|
120
|
+
|
121
|
+
search_words = words.dup
|
122
|
+
|
123
|
+
while search_words.length > 0
|
124
|
+
root = search_tree[:by_name]
|
125
|
+
|
126
|
+
search_words.each do |word|
|
127
|
+
if root = root[word]
|
128
|
+
if root[:value]
|
129
|
+
currencies.concat(root[:value])
|
130
|
+
end
|
131
|
+
else
|
132
|
+
break
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
search_words.delete_at(0)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def prepare_reply
|
141
|
+
codes = currencies.map do |currency|
|
142
|
+
currency[:iso_code]
|
143
|
+
end
|
144
|
+
codes.uniq!
|
145
|
+
codes.sort!
|
146
|
+
codes
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|