money 6.7.0 → 6.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -1
  3. data/.travis.yml +22 -5
  4. data/AUTHORS +5 -0
  5. data/CHANGELOG.md +109 -3
  6. data/Gemfile +13 -4
  7. data/LICENSE +2 -0
  8. data/README.md +69 -49
  9. data/config/currency_backwards_compatible.json +30 -0
  10. data/config/currency_iso.json +139 -62
  11. data/config/currency_non_iso.json +66 -2
  12. data/lib/money.rb +0 -13
  13. data/lib/money/bank/variable_exchange.rb +9 -22
  14. data/lib/money/currency.rb +35 -38
  15. data/lib/money/currency/heuristics.rb +1 -144
  16. data/lib/money/currency/loader.rb +1 -1
  17. data/lib/money/locale_backend/base.rb +7 -0
  18. data/lib/money/locale_backend/errors.rb +6 -0
  19. data/lib/money/locale_backend/i18n.rb +24 -0
  20. data/lib/money/locale_backend/legacy.rb +28 -0
  21. data/lib/money/money.rb +120 -151
  22. data/lib/money/money/allocation.rb +37 -0
  23. data/lib/money/money/arithmetic.rb +57 -52
  24. data/lib/money/money/constructors.rb +1 -2
  25. data/lib/money/money/formatter.rb +397 -0
  26. data/lib/money/money/formatting_rules.rb +120 -0
  27. data/lib/money/money/locale_backend.rb +20 -0
  28. data/lib/money/rates_store/memory.rb +1 -2
  29. data/lib/money/version.rb +1 -1
  30. data/money.gemspec +10 -16
  31. data/spec/bank/variable_exchange_spec.rb +7 -3
  32. data/spec/currency/heuristics_spec.rb +2 -153
  33. data/spec/currency_spec.rb +45 -4
  34. data/spec/locale_backend/i18n_spec.rb +62 -0
  35. data/spec/locale_backend/legacy_spec.rb +74 -0
  36. data/spec/money/allocation_spec.rb +130 -0
  37. data/spec/money/arithmetic_spec.rb +217 -104
  38. data/spec/money/constructors_spec.rb +0 -12
  39. data/spec/money/formatting_spec.rb +320 -179
  40. data/spec/money/locale_backend_spec.rb +14 -0
  41. data/spec/money_spec.rb +159 -26
  42. data/spec/rates_store/memory_spec.rb +13 -2
  43. data/spec/spec_helper.rb +2 -0
  44. data/spec/support/shared_examples/money_examples.rb +14 -0
  45. metadata +32 -41
  46. data/lib/money/money/formatting.rb +0 -417
@@ -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.new(from.fractional.to_s) / (
122
- BigDecimal.new(from.currency.subunit_to_unit.to_s) /
123
- BigDecimal.new(to_currency.subunit_to_unit.to_s)
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 = (fractional * BigDecimal.new(rate.to_s)).to_f
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.to_s.to_d
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 = case format
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 = case format
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)
@@ -8,7 +8,7 @@ class Money
8
8
 
9
9
  # Represents a specific currency unit.
10
10
  #
11
- # @see http://en.wikipedia.org/wiki/Currency
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
- id, _ = self.table.find{|key, currency| currency[:iso_numeric] == num}
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
- # http://www.answers.com/topic/monetary-unit
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
- # http://www.answers.com/topic/fractional-monetary-unit-subunit
119
+ # https://www.answers.com/topic/fractional-monetary-unit-subunit
119
120
  #
120
- # See http://en.wikipedia.org/wiki/List_of_circulating_currencies and
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 = stringify_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 = stringify_keys if existed
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 Fixnum hash value based on the +id+ attribute in order to use
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 [Fixnum]
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
- # Conversation to +self+.
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 the number of digits after the decimal separator.
404
+ # Returns if a code currency is ISO.
396
405
  #
397
- # @return [Float]
398
- def exponent
399
- Math.log10(@subunit_to_unit)
400
- end
401
-
402
- # Cache decimal places for subunit_to_unit values. Common ones pre-cached.
403
- def self.decimal_places_cache
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
- # The number of decimal places needed.
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 decimal_places
411
- cache[subunit_to_unit] ||= calculate_decimal_places(subunit_to_unit)
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
- return Analyzer.new(str, search_tree).process
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, :symbolize_names => true)
20
+ JSON.parse(json, symbolize_names: true)
21
21
  end
22
22
  end
23
23
  end
@@ -0,0 +1,7 @@
1
+ require 'money/locale_backend/errors'
2
+
3
+ class Money
4
+ module LocaleBackend
5
+ class Base; end
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ class Money
2
+ module LocaleBackend
3
+ class NotSupported < StandardError; end
4
+ class Unknown < ArgumentError; end
5
+ end
6
+ 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