money 6.5.1 → 6.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +209 -5
- data/LICENSE +18 -16
- data/README.md +321 -70
- data/config/currency_backwards_compatible.json +65 -0
- data/config/currency_iso.json +280 -94
- data/config/currency_non_iso.json +101 -3
- data/lib/money/bank/base.rb +1 -3
- data/lib/money/bank/variable_exchange.rb +88 -96
- data/lib/money/currency/heuristics.rb +1 -143
- data/lib/money/currency/loader.rb +15 -13
- data/lib/money/currency.rb +98 -81
- data/lib/money/locale_backend/base.rb +7 -0
- data/lib/money/locale_backend/currency.rb +11 -0
- data/lib/money/locale_backend/errors.rb +6 -0
- data/lib/money/locale_backend/i18n.rb +25 -0
- data/lib/money/locale_backend/legacy.rb +28 -0
- data/lib/money/money/allocation.rb +46 -0
- data/lib/money/money/arithmetic.rb +97 -52
- data/lib/money/money/constructors.rb +5 -6
- data/lib/money/money/formatter.rb +399 -0
- data/lib/money/money/formatting_rules.rb +142 -0
- data/lib/money/money/locale_backend.rb +22 -0
- data/lib/money/money.rb +268 -194
- data/lib/money/rates_store/memory.rb +120 -0
- data/lib/money/version.rb +1 -1
- data/money.gemspec +15 -20
- metadata +36 -59
- data/.coveralls.yml +0 -1
- data/.gitignore +0 -22
- data/.travis.yml +0 -13
- data/AUTHORS +0 -116
- data/CONTRIBUTING.md +0 -17
- data/Gemfile +0 -7
- data/Rakefile +0 -17
- data/lib/money/money/formatting.rb +0 -386
- data/spec/bank/base_spec.rb +0 -77
- data/spec/bank/single_currency_spec.rb +0 -11
- data/spec/bank/variable_exchange_spec.rb +0 -275
- data/spec/currency/heuristics_spec.rb +0 -84
- data/spec/currency_spec.rb +0 -321
- data/spec/money/arithmetic_spec.rb +0 -568
- data/spec/money/constructors_spec.rb +0 -75
- data/spec/money/formatting_spec.rb +0 -667
- data/spec/money_spec.rb +0 -745
- data/spec/spec_helper.rb +0 -23
data/lib/money/currency.rb
CHANGED
@@ -8,18 +8,44 @@ 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
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
119
|
+
# https://www.answers.com/topic/fractional-monetary-unit-subunit
|
92
120
|
#
|
93
|
-
# See
|
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.
|
131
|
+
# Money::Currency.all()
|
104
132
|
# [#<Currency ..USD>, 'CAD', 'EUR']...
|
105
133
|
def all
|
106
|
-
table.keys.map
|
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 =
|
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 =
|
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]
|
202
|
-
# @return [String] character used to separate thousands grouping of
|
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
|
-
#
|
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 [
|
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
|
-
#
|
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
|
408
|
+
# Returns if a code currency is ISO.
|
381
409
|
#
|
382
|
-
# @return [
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
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
|
-
#
|
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
|
396
|
-
|
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,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
|