money 6.5.1 → 6.16.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 +5 -5
  2. data/CHANGELOG.md +209 -5
  3. data/LICENSE +18 -16
  4. data/README.md +321 -70
  5. data/config/currency_backwards_compatible.json +65 -0
  6. data/config/currency_iso.json +280 -94
  7. data/config/currency_non_iso.json +101 -3
  8. data/lib/money/bank/base.rb +1 -3
  9. data/lib/money/bank/variable_exchange.rb +88 -96
  10. data/lib/money/currency/heuristics.rb +1 -143
  11. data/lib/money/currency/loader.rb +15 -13
  12. data/lib/money/currency.rb +98 -81
  13. data/lib/money/locale_backend/base.rb +7 -0
  14. data/lib/money/locale_backend/currency.rb +11 -0
  15. data/lib/money/locale_backend/errors.rb +6 -0
  16. data/lib/money/locale_backend/i18n.rb +25 -0
  17. data/lib/money/locale_backend/legacy.rb +28 -0
  18. data/lib/money/money/allocation.rb +46 -0
  19. data/lib/money/money/arithmetic.rb +97 -52
  20. data/lib/money/money/constructors.rb +5 -6
  21. data/lib/money/money/formatter.rb +399 -0
  22. data/lib/money/money/formatting_rules.rb +142 -0
  23. data/lib/money/money/locale_backend.rb +22 -0
  24. data/lib/money/money.rb +268 -194
  25. data/lib/money/rates_store/memory.rb +120 -0
  26. data/lib/money/version.rb +1 -1
  27. data/money.gemspec +15 -20
  28. metadata +36 -59
  29. data/.coveralls.yml +0 -1
  30. data/.gitignore +0 -22
  31. data/.travis.yml +0 -13
  32. data/AUTHORS +0 -116
  33. data/CONTRIBUTING.md +0 -17
  34. data/Gemfile +0 -7
  35. data/Rakefile +0 -17
  36. data/lib/money/money/formatting.rb +0 -386
  37. data/spec/bank/base_spec.rb +0 -77
  38. data/spec/bank/single_currency_spec.rb +0 -11
  39. data/spec/bank/variable_exchange_spec.rb +0 -275
  40. data/spec/currency/heuristics_spec.rb +0 -84
  41. data/spec/currency_spec.rb +0 -321
  42. data/spec/money/arithmetic_spec.rb +0 -568
  43. data/spec/money/constructors_spec.rb +0 -75
  44. data/spec/money/formatting_spec.rb +0 -667
  45. data/spec/money_spec.rb +0 -745
  46. data/spec/spec_helper.rb +0 -23
@@ -8,18 +8,44 @@ 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
15
15
  extend Enumerable
16
- extend Money::Currency::Loader
17
16
  extend Money::Currency::Heuristics
18
17
 
18
+ # Keeping cached instances in sync between threads
19
+ @@mutex = Mutex.new
20
+ @@instances = {}
21
+
22
+ # Thrown when a Currency has been registered without all the attributes
23
+ # which are required for the current action.
24
+ class MissingAttributeError < StandardError
25
+ def initialize(method, currency, attribute)
26
+ super(
27
+ "Can't call Currency.#{method} - currency '#{currency}' is missing "\
28
+ "the attribute '#{attribute}'"
29
+ )
30
+ end
31
+ end
32
+
19
33
  # Thrown when an unknown currency is requested.
20
34
  class UnknownCurrency < ArgumentError; end
21
35
 
22
36
  class << self
37
+ def new(id)
38
+ id = id.to_s.downcase
39
+ unless stringified_keys.include?(id)
40
+ raise UnknownCurrency, "Unknown currency '#{id}'"
41
+ end
42
+
43
+ _instances[id] || @@mutex.synchronize { _instances[id] ||= super }
44
+ end
45
+
46
+ def _instances
47
+ @@instances
48
+ end
23
49
 
24
50
  # Lookup a currency with given +id+ an returns a +Currency+ instance on
25
51
  # success, +nil+ otherwise.
@@ -49,10 +75,12 @@ class Money
49
75
  #
50
76
  # @example
51
77
  # Money::Currency.find_by_iso_numeric(978) #=> #<Money::Currency id: eur ...>
78
+ # Money::Currency.find_by_iso_numeric(51) #=> #<Money::Currency id: amd ...>
52
79
  # Money::Currency.find_by_iso_numeric('001') #=> nil
53
80
  def find_by_iso_numeric(num)
54
- num = num.to_s
55
- id, _ = self.table.find{|key, currency| currency[:iso_numeric] == num}
81
+ num = num.to_s.rjust(3, '0')
82
+ return if num.empty?
83
+ id, _ = self.table.find { |key, currency| currency[:iso_numeric] == num }
56
84
  new(id)
57
85
  rescue UnknownCurrency
58
86
  nil
@@ -85,25 +113,31 @@ class Money
85
113
  #
86
114
  # == monetary unit
87
115
  # The standard unit of value of a currency, as the dollar in the United States or the peso in Mexico.
88
- # http://www.answers.com/topic/monetary-unit
116
+ # https://www.answers.com/topic/monetary-unit
89
117
  # == fractional monetary unit, subunit
90
118
  # A monetary unit that is valued at a fraction (usually one hundredth) of the basic monetary unit
91
- # http://www.answers.com/topic/fractional-monetary-unit-subunit
119
+ # https://www.answers.com/topic/fractional-monetary-unit-subunit
92
120
  #
93
- # See http://en.wikipedia.org/wiki/List_of_circulating_currencies and
121
+ # See https://en.wikipedia.org/wiki/List_of_circulating_currencies and
94
122
  # http://search.cpan.org/~tnguyen/Locale-Currency-Format-1.28/Format.pm
95
123
  def table
96
- @table ||= load_currencies
124
+ @table ||= Loader.load_currencies
97
125
  end
98
126
 
99
127
  # List the currencies imported and registered
100
128
  # @return [Array]
101
129
  #
102
130
  # @example
103
- # Money::Currency.iso_codes()
131
+ # Money::Currency.all()
104
132
  # [#<Currency ..USD>, 'CAD', 'EUR']...
105
133
  def all
106
- table.keys.map {|curr| Currency.new(curr)}.sort_by(&:priority)
134
+ table.keys.map do |curr|
135
+ c = Currency.new(curr)
136
+ if c.priority.nil?
137
+ raise MissingAttributeError.new(:all, c.id, :priority)
138
+ end
139
+ c
140
+ end.sort_by(&:priority)
107
141
  end
108
142
 
109
143
  # We need a string-based validator before creating an unbounded number of
@@ -135,10 +169,20 @@ class Money
135
169
  # @option delimiter [String] character between each thousands place
136
170
  def register(curr)
137
171
  key = curr.fetch(:iso_code).downcase.to_sym
172
+ @@mutex.synchronize { _instances.delete(key.to_s) }
138
173
  @table[key] = curr
139
- @stringified_keys = stringify_keys
174
+ @stringified_keys = nil
140
175
  end
141
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
142
186
 
143
187
  # Unregister a currency.
144
188
  #
@@ -154,15 +198,18 @@ class Money
154
198
  key = curr.downcase.to_sym
155
199
  end
156
200
  existed = @table.delete(key)
157
- @stringified_keys = stringify_keys
201
+ @stringified_keys = nil if existed
158
202
  existed ? true : false
159
203
  end
160
204
 
161
-
162
205
  def each
163
206
  all.each { |c| yield(c) }
164
207
  end
165
208
 
209
+ def reset!
210
+ @@instances = {}
211
+ @table = Loader.load_currencies
212
+ end
166
213
 
167
214
  private
168
215
 
@@ -171,49 +218,50 @@ class Money
171
218
  end
172
219
  end
173
220
 
174
- # @!attribute [r] id
221
+ # @!attribute [r] id
175
222
  # @return [Symbol] The symbol used to identify the currency, usually THE
176
223
  # lowercase +iso_code+ attribute.
177
- # @!attribute [r] priority
224
+ # @!attribute [r] priority
178
225
  # @return [Integer] A numerical value you can use to sort/group the
179
226
  # currency list.
180
- # @!attribute [r] iso_code
227
+ # @!attribute [r] iso_code
181
228
  # @return [String] The international 3-letter code as defined by the ISO
182
229
  # 4217 standard.
183
- # @!attribute [r] iso_numeric
230
+ # @!attribute [r] iso_numeric
184
231
  # @return [String] The international 3-numeric code as defined by the ISO
185
232
  # 4217 standard.
186
- # @!attribute [r] name
233
+ # @!attribute [r] name
187
234
  # @return [String] The currency name.
188
- # @!attribute [r] symbol
235
+ # @!attribute [r] symbol
189
236
  # @return [String] The currency symbol (UTF-8 encoded).
190
- # @!attribute [r] disambiguate_symbol
237
+ # @!attribute [r] disambiguate_symbol
191
238
  # @return [String] Alternative currency used if symbol is ambiguous
192
- # @!attribute [r] html_entity
239
+ # @!attribute [r] html_entity
193
240
  # @return [String] The html entity for the currency symbol
194
- # @!attribute [r] subunit
241
+ # @!attribute [r] subunit
195
242
  # @return [String] The name of the fractional monetary unit.
196
- # @!attribute [r] subunit_to_unit
243
+ # @!attribute [r] subunit_to_unit
197
244
  # @return [Integer] The proportion between the unit and the subunit
198
- # @!attribute [r] decimal_mark
245
+ # @!attribute [r] decimal_mark
199
246
  # @return [String] The decimal mark, or character used to separate the
200
247
  # whole unit from the subunit.
201
- # @!attribute [r] The
202
- # @return [String] character used to separate thousands grouping of the
203
- # whole unit.
204
- # @!attribute [r] symbol_first
248
+ # @!attribute [r] thousands_separator
249
+ # @return [String] The character used to separate thousands grouping of
250
+ # the whole unit.
251
+ # @!attribute [r] symbol_first
205
252
  # @return [Boolean] Should the currency symbol precede the amount, or
206
253
  # should it come after?
207
- # @!attribute [r] smallest_denomination
254
+ # @!attribute [r] smallest_denomination
208
255
  # @return [Integer] Smallest amount of cash possible (in the subunit of
209
256
  # this currency)
210
257
 
211
258
  attr_reader :id, :priority, :iso_code, :iso_numeric, :name, :symbol,
212
259
  :disambiguate_symbol, :html_entity, :subunit, :subunit_to_unit, :decimal_mark,
213
- :thousands_separator, :symbol_first, :smallest_denomination
260
+ :thousands_separator, :symbol_first, :smallest_denomination, :format
214
261
 
215
262
  alias_method :separator, :decimal_mark
216
263
  alias_method :delimiter, :thousands_separator
264
+ alias_method :eql?, :==
217
265
 
218
266
  # Create a new +Currency+ object.
219
267
  #
@@ -225,10 +273,6 @@ class Money
225
273
  # @example
226
274
  # Money::Currency.new(:usd) #=> #<Money::Currency id: usd ...>
227
275
  def initialize(id)
228
- id = id.to_s.downcase
229
- unless self.class.stringified_keys.include?(id)
230
- raise UnknownCurrency, "Unknown currency '#{id}'"
231
- end
232
276
  @id = id.to_sym
233
277
  initialize_data!
234
278
  end
@@ -283,26 +327,10 @@ class Money
283
327
  end
284
328
  private :compare_ids
285
329
 
286
- # Compares +self+ with +other_currency+ and returns +true+ if the are the
287
- # same or if their +id+ attributes match.
288
- #
289
- # @param [Money::Currency] other_currency The currency to compare to.
290
- #
291
- # @return [Boolean]
292
- #
293
- # @example
294
- # c1 = Money::Currency.new(:usd)
295
- # c2 = Money::Currency.new(:jpy)
296
- # c1.eql? c1 #=> true
297
- # c1.eql? c2 #=> false
298
- def eql?(other_currency)
299
- self == other_currency
300
- end
301
-
302
- # Returns a Fixnum hash value based on the +id+ attribute in order to use
330
+ # Returns a Integer hash value based on the +id+ attribute in order to use
303
331
  # functions like & (intersection), group_by, etc.
304
332
  #
305
- # @return [Fixnum]
333
+ # @return [Integer]
306
334
  #
307
335
  # @example
308
336
  # Money::Currency.new(:usd).hash #=> 428936
@@ -317,7 +345,7 @@ class Money
317
345
  # @example
318
346
  # Money::Currency.new(:usd) #=> #<Currency id: usd ...>
319
347
  def inspect
320
- "#<#{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}, smallest_denomination: #{smallest_denomination}>"
348
+ "#<#{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}, smallest_denomination: #{smallest_denomination}, format: #{format}>"
321
349
  end
322
350
 
323
351
  # Returns a string representation corresponding to the upcase +id+
@@ -359,7 +387,7 @@ class Money
359
387
  id.to_s.upcase.to_sym
360
388
  end
361
389
 
362
- # Conversation to +self+.
390
+ # Conversion to +self+.
363
391
  #
364
392
  # @return [self]
365
393
  def to_currency
@@ -377,42 +405,30 @@ class Money
377
405
  !!@symbol_first
378
406
  end
379
407
 
380
- # Returns the number of digits after the decimal separator.
408
+ # Returns if a code currency is ISO.
381
409
  #
382
- # @return [Float]
383
- def exponent
384
- Math.log10(@subunit_to_unit)
385
- end
386
-
387
- # Cache decimal places for subunit_to_unit values. Common ones pre-cached.
388
- def self.decimal_places_cache
389
- @decimal_places_cache ||= {1 => 0, 10 => 1, 100 => 2, 1000 => 3}
410
+ # @return [Boolean]
411
+ #
412
+ # @example
413
+ # Money::Currency.new(:usd).iso?
414
+ #
415
+ def iso?
416
+ iso_numeric && iso_numeric != ''
390
417
  end
391
418
 
392
- # The number of decimal places needed.
419
+ # Returns the relation between subunit and unit as a base 10 exponent.
420
+ #
421
+ # Note that MGA and MRU are exceptions and are rounded to 1
422
+ # @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes
393
423
  #
394
424
  # @return [Integer]
395
- def decimal_places
396
- cache[subunit_to_unit] ||= calculate_decimal_places(subunit_to_unit)
425
+ def exponent
426
+ Math.log10(subunit_to_unit).round
397
427
  end
428
+ alias decimal_places exponent
398
429
 
399
430
  private
400
431
 
401
- def cache
402
- self.class.decimal_places_cache
403
- end
404
-
405
- # If we need to figure out how many decimal places we need we
406
- # use repeated integer division.
407
- def calculate_decimal_places(num)
408
- i = 1
409
- while num >= 10
410
- num /= 10
411
- i += 1 if num >= 10
412
- end
413
- i
414
- end
415
-
416
432
  def initialize_data!
417
433
  data = self.class.table[@id]
418
434
  @alternate_symbols = data[:alternate_symbols]
@@ -429,6 +445,7 @@ class Money
429
445
  @symbol = data[:symbol]
430
446
  @symbol_first = data[:symbol_first]
431
447
  @thousands_separator = data[:thousands_separator]
448
+ @format = data[:format]
432
449
  end
433
450
  end
434
451
  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,11 @@
1
+ require 'money/locale_backend/base'
2
+
3
+ class Money
4
+ module LocaleBackend
5
+ class Currency < Base
6
+ def lookup(key, currency)
7
+ currency.public_send(key) if currency.respond_to?(key)
8
+ end
9
+ end
10
+ end
11
+ 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,25 @@
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
+ symbol: :unit
10
+ }.freeze
11
+
12
+ def initialize
13
+ raise NotSupported, 'I18n not found' unless defined?(::I18n)
14
+ end
15
+
16
+ def lookup(key, _)
17
+ i18n_key = KEY_MAP[key]
18
+
19
+ ::I18n.t i18n_key, scope: 'number.currency.format', raise: true
20
+ rescue ::I18n::MissingTranslationData
21
+ ::I18n.t i18n_key, scope: 'number.format', default: nil
22
+ end
23
+ end
24
+ end
25
+ 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
+ warn '[DEPRECATION] You are using the default localization behaviour that will change in the next major release. Find out more - https://github.com/RubyMoney/money#deprecation'
13
+
14
+ if Money.use_i18n
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
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ class Money
4
+ class Allocation
5
+ # Splits a given amount in parts without losing pennies.
6
+ # The left-over pennies will be distributed round-robin amongst the parts. This means that
7
+ # parts listed first will likely receive more pennies than the ones listed later.
8
+ #
9
+ # The results should always add up to the original amount.
10
+ #
11
+ # The parts can be specified as:
12
+ # Numeric — performs the split between a given number of parties evenely
13
+ # Array<Numeric> — allocates the amounts proportionally to the given array
14
+ #
15
+ def self.generate(amount, parts, whole_amounts = true)
16
+ parts = if parts.is_a?(Numeric)
17
+ Array.new(parts, 1)
18
+ elsif parts.all?(&:zero?)
19
+ Array.new(parts.count, 1)
20
+ else
21
+ parts.dup
22
+ end
23
+
24
+ raise ArgumentError, 'need at least one party' if parts.empty?
25
+
26
+ result = []
27
+ remaining_amount = amount
28
+
29
+ until parts.empty? do
30
+ parts_sum = parts.inject(0, :+)
31
+ part = parts.pop
32
+
33
+ current_split = 0
34
+ if parts_sum > 0
35
+ current_split = remaining_amount * part / parts_sum
36
+ current_split = current_split.truncate if whole_amounts
37
+ end
38
+
39
+ result.unshift current_split
40
+ remaining_amount -= current_split
41
+ end
42
+
43
+ result
44
+ end
45
+ end
46
+ end