money 6.7.0 → 6.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +2 -1
- data/.travis.yml +22 -5
- data/AUTHORS +5 -0
- data/CHANGELOG.md +109 -3
- data/Gemfile +13 -4
- data/LICENSE +2 -0
- data/README.md +69 -49
- data/config/currency_backwards_compatible.json +30 -0
- data/config/currency_iso.json +139 -62
- data/config/currency_non_iso.json +66 -2
- data/lib/money.rb +0 -13
- data/lib/money/bank/variable_exchange.rb +9 -22
- data/lib/money/currency.rb +35 -38
- data/lib/money/currency/heuristics.rb +1 -144
- data/lib/money/currency/loader.rb +1 -1
- data/lib/money/locale_backend/base.rb +7 -0
- data/lib/money/locale_backend/errors.rb +6 -0
- data/lib/money/locale_backend/i18n.rb +24 -0
- data/lib/money/locale_backend/legacy.rb +28 -0
- data/lib/money/money.rb +120 -151
- data/lib/money/money/allocation.rb +37 -0
- data/lib/money/money/arithmetic.rb +57 -52
- data/lib/money/money/constructors.rb +1 -2
- data/lib/money/money/formatter.rb +397 -0
- data/lib/money/money/formatting_rules.rb +120 -0
- data/lib/money/money/locale_backend.rb +20 -0
- data/lib/money/rates_store/memory.rb +1 -2
- data/lib/money/version.rb +1 -1
- data/money.gemspec +10 -16
- data/spec/bank/variable_exchange_spec.rb +7 -3
- data/spec/currency/heuristics_spec.rb +2 -153
- data/spec/currency_spec.rb +45 -4
- data/spec/locale_backend/i18n_spec.rb +62 -0
- data/spec/locale_backend/legacy_spec.rb +74 -0
- data/spec/money/allocation_spec.rb +130 -0
- data/spec/money/arithmetic_spec.rb +217 -104
- data/spec/money/constructors_spec.rb +0 -12
- data/spec/money/formatting_spec.rb +320 -179
- data/spec/money/locale_backend_spec.rb +14 -0
- data/spec/money_spec.rb +159 -26
- data/spec/rates_store/memory_spec.rb +13 -2
- data/spec/spec_helper.rb +2 -0
- data/spec/support/shared_examples/money_examples.rb +14 -0
- metadata +32 -41
- data/lib/money/money/formatting.rb +0 -417
data/lib/money.rb
CHANGED
@@ -2,18 +2,5 @@ require "bigdecimal"
|
|
2
2
|
require "bigdecimal/util"
|
3
3
|
require "set"
|
4
4
|
require "i18n"
|
5
|
-
require "sixarm_ruby_unaccent"
|
6
5
|
require "money/currency"
|
7
6
|
require "money/money"
|
8
|
-
|
9
|
-
# Overwrites unaccent method of sixarm_ruby_unaccent.
|
10
|
-
class String
|
11
|
-
def unaccent
|
12
|
-
accentmap = ACCENTMAP
|
13
|
-
accentmap.delete("\u{0142}") # Delete ł symbol from ACCENTMAP used in PLN currency
|
14
|
-
accentmap.delete("\u{010D}") # Delete č symbol from ACCENTMAP used in CZK currency
|
15
|
-
accentmap.delete("\u{FDFC}") # Delete ﷼ symbol from ACCENTMAP used in IRR, SAR and YER currencies
|
16
|
-
accentmap.delete("\u{20A8}") # Delete ₨ symbol from ACCENTMAP used in INR, LKR, MUR, NPR, PKR and SCR currencies
|
17
|
-
split(//u).map {|c| accentmap[c] || c }.join("")
|
18
|
-
end
|
19
|
-
end
|
@@ -45,8 +45,9 @@ class Money
|
|
45
45
|
attr_reader :mutex, :store
|
46
46
|
|
47
47
|
# Available formats for importing/exporting rates.
|
48
|
-
RATE_FORMATS = [:json, :ruby, :yaml]
|
48
|
+
RATE_FORMATS = [:json, :ruby, :yaml].freeze
|
49
49
|
SERIALIZER_SEPARATOR = '_TO_'.freeze
|
50
|
+
FORMAT_SERIALIZERS = {json: JSON, ruby: Marshal, yaml: YAML}.freeze
|
50
51
|
|
51
52
|
# Initializes a new +Money::Bank::VariableExchange+ object.
|
52
53
|
# It defaults to using an in-memory, thread safe store instance for
|
@@ -118,20 +119,20 @@ class Money
|
|
118
119
|
end
|
119
120
|
|
120
121
|
def calculate_fractional(from, to_currency)
|
121
|
-
BigDecimal
|
122
|
-
BigDecimal
|
123
|
-
BigDecimal
|
122
|
+
BigDecimal(from.fractional.to_s) / (
|
123
|
+
BigDecimal(from.currency.subunit_to_unit.to_s) /
|
124
|
+
BigDecimal(to_currency.subunit_to_unit.to_s)
|
124
125
|
)
|
125
126
|
end
|
126
127
|
|
127
128
|
def exchange(fractional, rate, &block)
|
128
|
-
ex =
|
129
|
+
ex = fractional * BigDecimal(rate.to_s)
|
129
130
|
if block_given?
|
130
131
|
yield ex
|
131
132
|
elsif @rounding_method
|
132
133
|
@rounding_method.call(ex)
|
133
134
|
else
|
134
|
-
ex
|
135
|
+
ex
|
135
136
|
end
|
136
137
|
end
|
137
138
|
|
@@ -216,14 +217,7 @@ class Money
|
|
216
217
|
RATE_FORMATS.include? format
|
217
218
|
|
218
219
|
store.transaction do
|
219
|
-
s =
|
220
|
-
when :json
|
221
|
-
JSON.dump(rates)
|
222
|
-
when :ruby
|
223
|
-
Marshal.dump(rates)
|
224
|
-
when :yaml
|
225
|
-
YAML.dump(rates)
|
226
|
-
end
|
220
|
+
s = FORMAT_SERIALIZERS[format].dump(rates)
|
227
221
|
|
228
222
|
unless file.nil?
|
229
223
|
File.open(file, "w") {|f| f.write(s) }
|
@@ -264,14 +258,7 @@ class Money
|
|
264
258
|
RATE_FORMATS.include? format
|
265
259
|
|
266
260
|
store.transaction do
|
267
|
-
data =
|
268
|
-
when :json
|
269
|
-
JSON.load(s)
|
270
|
-
when :ruby
|
271
|
-
Marshal.load(s)
|
272
|
-
when :yaml
|
273
|
-
YAML.load(s)
|
274
|
-
end
|
261
|
+
data = FORMAT_SERIALIZERS[format].load(s)
|
275
262
|
|
276
263
|
data.each do |key, rate|
|
277
264
|
from, to = key.split(SERIALIZER_SEPARATOR)
|
data/lib/money/currency.rb
CHANGED
@@ -8,7 +8,7 @@ class Money
|
|
8
8
|
|
9
9
|
# Represents a specific currency unit.
|
10
10
|
#
|
11
|
-
# @see
|
11
|
+
# @see https://en.wikipedia.org/wiki/Currency
|
12
12
|
# @see http://iso4217.net/
|
13
13
|
class Currency
|
14
14
|
include Comparable
|
@@ -79,7 +79,8 @@ class Money
|
|
79
79
|
# Money::Currency.find_by_iso_numeric('001') #=> nil
|
80
80
|
def find_by_iso_numeric(num)
|
81
81
|
num = num.to_s
|
82
|
-
|
82
|
+
return if num.empty?
|
83
|
+
id, _ = self.table.find { |key, currency| currency[:iso_numeric] == num }
|
83
84
|
new(id)
|
84
85
|
rescue UnknownCurrency
|
85
86
|
nil
|
@@ -112,12 +113,12 @@ class Money
|
|
112
113
|
#
|
113
114
|
# == monetary unit
|
114
115
|
# The standard unit of value of a currency, as the dollar in the United States or the peso in Mexico.
|
115
|
-
#
|
116
|
+
# https://www.answers.com/topic/monetary-unit
|
116
117
|
# == fractional monetary unit, subunit
|
117
118
|
# A monetary unit that is valued at a fraction (usually one hundredth) of the basic monetary unit
|
118
|
-
#
|
119
|
+
# https://www.answers.com/topic/fractional-monetary-unit-subunit
|
119
120
|
#
|
120
|
-
# See
|
121
|
+
# See https://en.wikipedia.org/wiki/List_of_circulating_currencies and
|
121
122
|
# http://search.cpan.org/~tnguyen/Locale-Currency-Format-1.28/Format.pm
|
122
123
|
def table
|
123
124
|
@table ||= load_currencies
|
@@ -170,9 +171,18 @@ class Money
|
|
170
171
|
key = curr.fetch(:iso_code).downcase.to_sym
|
171
172
|
@@mutex.synchronize { _instances.delete(key.to_s) }
|
172
173
|
@table[key] = curr
|
173
|
-
@stringified_keys =
|
174
|
+
@stringified_keys = nil
|
174
175
|
end
|
175
176
|
|
177
|
+
# Inherit a new currency from existing one
|
178
|
+
#
|
179
|
+
# @param parent_iso_code [String] the international 3-letter code as defined
|
180
|
+
# @param curr [Hash] See {register} method for hash structure
|
181
|
+
def inherit(parent_iso_code, curr)
|
182
|
+
parent_iso_code = parent_iso_code.downcase.to_sym
|
183
|
+
curr = @table.fetch(parent_iso_code, {}).merge(curr)
|
184
|
+
register(curr)
|
185
|
+
end
|
176
186
|
|
177
187
|
# Unregister a currency.
|
178
188
|
#
|
@@ -188,11 +198,10 @@ class Money
|
|
188
198
|
key = curr.downcase.to_sym
|
189
199
|
end
|
190
200
|
existed = @table.delete(key)
|
191
|
-
@stringified_keys =
|
201
|
+
@stringified_keys = nil if existed
|
192
202
|
existed ? true : false
|
193
203
|
end
|
194
204
|
|
195
|
-
|
196
205
|
def each
|
197
206
|
all.each { |c| yield(c) }
|
198
207
|
end
|
@@ -314,10 +323,10 @@ class Money
|
|
314
323
|
end
|
315
324
|
private :compare_ids
|
316
325
|
|
317
|
-
# Returns a
|
326
|
+
# Returns a Integer hash value based on the +id+ attribute in order to use
|
318
327
|
# functions like & (intersection), group_by, etc.
|
319
328
|
#
|
320
|
-
# @return [
|
329
|
+
# @return [Integer]
|
321
330
|
#
|
322
331
|
# @example
|
323
332
|
# Money::Currency.new(:usd).hash #=> 428936
|
@@ -374,7 +383,7 @@ class Money
|
|
374
383
|
id.to_s.upcase.to_sym
|
375
384
|
end
|
376
385
|
|
377
|
-
#
|
386
|
+
# Conversion to +self+.
|
378
387
|
#
|
379
388
|
# @return [self]
|
380
389
|
def to_currency
|
@@ -392,42 +401,30 @@ class Money
|
|
392
401
|
!!@symbol_first
|
393
402
|
end
|
394
403
|
|
395
|
-
# Returns
|
404
|
+
# Returns if a code currency is ISO.
|
396
405
|
#
|
397
|
-
# @return [
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
@decimal_places_cache ||= {1 => 0, 10 => 1, 100 => 2, 1000 => 3}
|
406
|
+
# @return [Boolean]
|
407
|
+
#
|
408
|
+
# @example
|
409
|
+
# Money::Currency.new(:usd).iso?
|
410
|
+
#
|
411
|
+
def iso?
|
412
|
+
iso_numeric && iso_numeric != ''
|
405
413
|
end
|
406
414
|
|
407
|
-
#
|
415
|
+
# Returns the relation between subunit and unit as a base 10 exponent.
|
416
|
+
#
|
417
|
+
# Note that MGA and MRO are exceptions and are rounded to 1
|
418
|
+
# @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes
|
408
419
|
#
|
409
420
|
# @return [Integer]
|
410
|
-
def
|
411
|
-
|
421
|
+
def exponent
|
422
|
+
Math.log10(subunit_to_unit).round
|
412
423
|
end
|
424
|
+
alias decimal_places exponent
|
413
425
|
|
414
426
|
private
|
415
427
|
|
416
|
-
def cache
|
417
|
-
self.class.decimal_places_cache
|
418
|
-
end
|
419
|
-
|
420
|
-
# If we need to figure out how many decimal places we need we
|
421
|
-
# use repeated integer division.
|
422
|
-
def calculate_decimal_places(num)
|
423
|
-
i = 1
|
424
|
-
while num >= 10
|
425
|
-
num /= 10
|
426
|
-
i += 1 if num >= 10
|
427
|
-
end
|
428
|
-
i
|
429
|
-
end
|
430
|
-
|
431
428
|
def initialize_data!
|
432
429
|
data = self.class.table[@id]
|
433
430
|
@alternate_symbols = data[:alternate_symbols]
|
@@ -3,152 +3,9 @@
|
|
3
3
|
class Money
|
4
4
|
class Currency
|
5
5
|
module Heuristics
|
6
|
-
|
7
|
-
# An robust and efficient algorithm for finding currencies in
|
8
|
-
# text. Using several algorithms it can find symbols, iso codes and
|
9
|
-
# even names of currencies.
|
10
|
-
# Although not recommendable, it can also attempt to find the given
|
11
|
-
# currency in an entire sentence
|
12
|
-
#
|
13
|
-
# Returns: Array (matched results)
|
14
6
|
def analyze(str)
|
15
|
-
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
# Build a search tree from the currency database
|
21
|
-
def search_tree
|
22
|
-
@_search_tree ||= {
|
23
|
-
:by_symbol => currencies_by_symbol,
|
24
|
-
:by_iso_code => currencies_by_iso_code,
|
25
|
-
:by_name => currencies_by_name
|
26
|
-
}
|
27
|
-
end
|
28
|
-
|
29
|
-
def currencies_by_symbol
|
30
|
-
{}.tap do |r|
|
31
|
-
table.each do |dummy, c|
|
32
|
-
symbol = (c[:symbol]||"").downcase
|
33
|
-
symbol.chomp!('.')
|
34
|
-
(r[symbol] ||= []) << c
|
35
|
-
|
36
|
-
(c[:alternate_symbols]||[]).each do |ac|
|
37
|
-
ac = ac.downcase
|
38
|
-
ac.chomp!('.')
|
39
|
-
(r[ac] ||= []) << c
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def currencies_by_iso_code
|
46
|
-
{}.tap do |r|
|
47
|
-
table.each do |dummy,c|
|
48
|
-
(r[c[:iso_code].downcase] ||= []) << c
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def currencies_by_name
|
54
|
-
{}.tap do |r|
|
55
|
-
table.each do |dummy,c|
|
56
|
-
name_parts = c[:name].unaccent.downcase.split
|
57
|
-
name_parts.each {|part| part.chomp!('.')}
|
58
|
-
|
59
|
-
# construct one branch per word
|
60
|
-
root = r
|
61
|
-
while name_part = name_parts.shift
|
62
|
-
root = (root[name_part] ||= {})
|
63
|
-
end
|
64
|
-
|
65
|
-
# the leaf is a currency
|
66
|
-
(root[:value] ||= []) << c
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
class Analyzer
|
72
|
-
attr_reader :search_tree, :words
|
73
|
-
attr_accessor :str, :currencies
|
74
|
-
|
75
|
-
def initialize str, search_tree
|
76
|
-
@str = (str||'').dup
|
77
|
-
@search_tree = search_tree
|
78
|
-
@currencies = []
|
79
|
-
end
|
80
|
-
|
81
|
-
def process
|
82
|
-
format
|
83
|
-
return [] if str.empty?
|
84
|
-
|
85
|
-
search_by_symbol
|
86
|
-
search_by_iso_code
|
87
|
-
search_by_name
|
88
|
-
|
89
|
-
prepare_reply
|
90
|
-
end
|
91
|
-
|
92
|
-
def format
|
93
|
-
str.gsub!(/[\r\n\t]/,'')
|
94
|
-
str.gsub!(/[0-9][\.,:0-9]*[0-9]/,'')
|
95
|
-
str.gsub!(/[0-9]/, '')
|
96
|
-
str.downcase!
|
97
|
-
@words = str.unaccent.split
|
98
|
-
@words.each {|word| word.chomp!('.'); word.chomp!(',') }
|
99
|
-
end
|
100
|
-
|
101
|
-
def search_by_symbol
|
102
|
-
words.each do |word|
|
103
|
-
if found = search_tree[:by_symbol][word]
|
104
|
-
currencies.concat(found)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def search_by_iso_code
|
110
|
-
words.each do |word|
|
111
|
-
if found = search_tree[:by_iso_code][word]
|
112
|
-
currencies.concat(found)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
def search_by_name
|
118
|
-
# remember, the search tree by name is a construct of branches and leaf!
|
119
|
-
# We need to try every combination of words within the sentence, so we
|
120
|
-
# end up with a x^2 equation, which should be fine as most names are either
|
121
|
-
# one or two words, and this is multiplied with the words of given sentence
|
122
|
-
|
123
|
-
search_words = words.dup
|
124
|
-
|
125
|
-
while search_words.length > 0
|
126
|
-
root = search_tree[:by_name]
|
127
|
-
|
128
|
-
search_words.each do |word|
|
129
|
-
if root = root[word]
|
130
|
-
if root[:value]
|
131
|
-
currencies.concat(root[:value])
|
132
|
-
end
|
133
|
-
else
|
134
|
-
break
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
search_words.delete_at(0)
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
def prepare_reply
|
143
|
-
codes = currencies.map do |currency|
|
144
|
-
currency[:iso_code]
|
145
|
-
end
|
146
|
-
codes.uniq!
|
147
|
-
codes.sort!
|
148
|
-
codes
|
149
|
-
end
|
7
|
+
raise StandardError, 'Heuristics deprecated, add `gem "money-heuristics"` to Gemfile'
|
150
8
|
end
|
151
9
|
end
|
152
10
|
end
|
153
11
|
end
|
154
|
-
|
@@ -17,7 +17,7 @@ class Money
|
|
17
17
|
def parse_currency_file(filename)
|
18
18
|
json = File.read("#{DATA_PATH}/#{filename}")
|
19
19
|
json.force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
|
20
|
-
JSON.parse(json, :
|
20
|
+
JSON.parse(json, symbolize_names: true)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'money/locale_backend/base'
|
2
|
+
|
3
|
+
class Money
|
4
|
+
module LocaleBackend
|
5
|
+
class I18n < Base
|
6
|
+
KEY_MAP = {
|
7
|
+
thousands_separator: :delimiter,
|
8
|
+
decimal_mark: :separator
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
raise NotSupported, 'I18n not found' unless defined?(::I18n)
|
13
|
+
end
|
14
|
+
|
15
|
+
def lookup(key, _)
|
16
|
+
i18n_key = KEY_MAP[key]
|
17
|
+
|
18
|
+
::I18n.t i18n_key, scope: 'number.currency.format', raise: true
|
19
|
+
rescue ::I18n::MissingTranslationData
|
20
|
+
::I18n.t i18n_key, scope: 'number.format', default: nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'money/locale_backend/base'
|
2
|
+
require 'money/locale_backend/i18n'
|
3
|
+
|
4
|
+
class Money
|
5
|
+
module LocaleBackend
|
6
|
+
class Legacy < Base
|
7
|
+
def initialize
|
8
|
+
raise NotSupported, 'I18n not found' if Money.use_i18n && !defined?(::I18n)
|
9
|
+
end
|
10
|
+
|
11
|
+
def lookup(key, currency)
|
12
|
+
if Money.use_i18n
|
13
|
+
warn '[DEPRECATION] `use_i18n` is deprecated - use `Money.locale_backend = :i18n` instead'
|
14
|
+
|
15
|
+
i18n_backend.lookup(key, nil) || currency.public_send(key)
|
16
|
+
else
|
17
|
+
currency.public_send(key)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def i18n_backend
|
24
|
+
@i18n_backend ||= Money::LocaleBackend::I18n.new
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|