money 6.7.1 → 6.13.0
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.
- checksums.yaml +4 -4
- data/.rspec +2 -1
- data/.travis.yml +15 -6
- data/AUTHORS +5 -0
- data/CHANGELOG.md +98 -3
- data/Gemfile +13 -4
- data/LICENSE +2 -0
- data/README.md +64 -44
- data/config/currency_backwards_compatible.json +30 -0
- data/config/currency_iso.json +135 -59
- 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 +33 -39
- 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 +106 -139
- data/lib/money/money/allocation.rb +37 -0
- data/lib/money/money/arithmetic.rb +31 -28
- 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 +44 -3
- 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 +184 -90
- data/spec/money/constructors_spec.rb +0 -12
- data/spec/money/formatting_spec.rb +296 -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 -40
- data/lib/money/money/formatting.rb +0 -418
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
|
@@ -392,45 +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
|
-
#
|
398
|
-
# @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes
|
406
|
+
# @return [Boolean]
|
399
407
|
#
|
400
|
-
# @
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
# Cache decimal places for subunit_to_unit values. Common ones pre-cached.
|
406
|
-
def self.decimal_places_cache
|
407
|
-
@decimal_places_cache ||= {1 => 0, 10 => 1, 100 => 2, 1000 => 3}
|
408
|
+
# @example
|
409
|
+
# Money::Currency.new(:usd).iso?
|
410
|
+
#
|
411
|
+
def iso?
|
412
|
+
iso_numeric && iso_numeric != ''
|
408
413
|
end
|
409
414
|
|
410
|
-
#
|
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
|
411
419
|
#
|
412
420
|
# @return [Integer]
|
413
|
-
def
|
414
|
-
|
421
|
+
def exponent
|
422
|
+
Math.log10(subunit_to_unit).round
|
415
423
|
end
|
424
|
+
alias decimal_places exponent
|
416
425
|
|
417
426
|
private
|
418
427
|
|
419
|
-
def cache
|
420
|
-
self.class.decimal_places_cache
|
421
|
-
end
|
422
|
-
|
423
|
-
# If we need to figure out how many decimal places we need we
|
424
|
-
# use repeated integer division.
|
425
|
-
def calculate_decimal_places(num)
|
426
|
-
i = 1
|
427
|
-
while num >= 10
|
428
|
-
num /= 10
|
429
|
-
i += 1 if num >= 10
|
430
|
-
end
|
431
|
-
i
|
432
|
-
end
|
433
|
-
|
434
428
|
def initialize_data!
|
435
429
|
data = self.class.table[@id]
|
436
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
|