money 6.9.0 → 6.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +131 -3
  3. data/LICENSE +17 -17
  4. data/README.md +181 -71
  5. data/config/currency_backwards_compatible.json +65 -0
  6. data/config/currency_iso.json +119 -56
  7. data/config/currency_non_iso.json +35 -2
  8. data/lib/money/bank/variable_exchange.rb +22 -12
  9. data/lib/money/currency/loader.rb +15 -13
  10. data/lib/money/currency.rb +38 -39
  11. data/lib/money/locale_backend/base.rb +7 -0
  12. data/lib/money/locale_backend/currency.rb +11 -0
  13. data/lib/money/locale_backend/errors.rb +6 -0
  14. data/lib/money/locale_backend/i18n.rb +25 -0
  15. data/lib/money/locale_backend/legacy.rb +28 -0
  16. data/lib/money/money/allocation.rb +46 -0
  17. data/lib/money/money/arithmetic.rb +33 -15
  18. data/lib/money/money/constructors.rb +1 -2
  19. data/lib/money/money/formatter.rb +399 -0
  20. data/lib/money/money/formatting_rules.rb +142 -0
  21. data/lib/money/money/locale_backend.rb +22 -0
  22. data/lib/money/money.rb +235 -187
  23. data/lib/money/rates_store/memory.rb +24 -24
  24. data/lib/money/version.rb +1 -1
  25. data/money.gemspec +14 -8
  26. metadata +36 -56
  27. data/.coveralls.yml +0 -1
  28. data/.gitignore +0 -23
  29. data/.rspec +0 -1
  30. data/.travis.yml +0 -26
  31. data/AUTHORS +0 -126
  32. data/CONTRIBUTING.md +0 -17
  33. data/Gemfile +0 -16
  34. data/Rakefile +0 -17
  35. data/lib/money/money/formatting.rb +0 -426
  36. data/spec/bank/base_spec.rb +0 -79
  37. data/spec/bank/single_currency_spec.rb +0 -13
  38. data/spec/bank/variable_exchange_spec.rb +0 -265
  39. data/spec/currency/heuristics_spec.rb +0 -11
  40. data/spec/currency/loader_spec.rb +0 -19
  41. data/spec/currency_spec.rb +0 -359
  42. data/spec/money/arithmetic_spec.rb +0 -693
  43. data/spec/money/constructors_spec.rb +0 -103
  44. data/spec/money/formatting_spec.rb +0 -757
  45. data/spec/money_spec.rb +0 -778
  46. data/spec/rates_store/memory_spec.rb +0 -69
  47. data/spec/spec_helper.rb +0 -28
@@ -1,426 +0,0 @@
1
- # encoding: UTF-8
2
- class Money
3
- module Formatting
4
- # Creates a formatted price string according to several rules.
5
- #
6
- # @param [Hash] rules The options used to format the string.
7
- #
8
- # @return [String]
9
- #
10
- # @option rules [Boolean, String] :display_free (false) Whether a zero
11
- # amount of money should be formatted of "free" or as the supplied string.
12
- #
13
- # @example
14
- # Money.us_dollar(0).format(:display_free => true) #=> "free"
15
- # Money.us_dollar(0).format(:display_free => "gratis") #=> "gratis"
16
- # Money.us_dollar(0).format #=> "$0.00"
17
- #
18
- # @option rules [Boolean] :with_currency (false) Whether the currency name
19
- # should be appended to the result string.
20
- #
21
- # @example
22
- # Money.ca_dollar(100).format #=> "$1.00"
23
- # Money.ca_dollar(100).format(:with_currency => true) #=> "$1.00 CAD"
24
- # Money.us_dollar(85).format(:with_currency => true) #=> "$0.85 USD"
25
- #
26
- # @option rules [Boolean] :rounded_infinite_precision (false) Whether the
27
- # amount of money should be rounded when using {infinite_precision}
28
- #
29
- # @example
30
- # Money.us_dollar(100.1).format #=> "$1.001"
31
- # Money.us_dollar(100.1).format(:rounded_infinite_precision => true) #=> "$1"
32
- # Money.us_dollar(100.9).format(:rounded_infinite_precision => true) #=> "$1.01"
33
- #
34
- # @option rules [Boolean] :no_cents (false) Whether cents should be omitted.
35
- #
36
- # @example
37
- # Money.ca_dollar(100).format(:no_cents => true) #=> "$1"
38
- # Money.ca_dollar(599).format(:no_cents => true) #=> "$5"
39
- #
40
- # @option rules [Boolean] :no_cents_if_whole (false) Whether cents should be
41
- # omitted if the cent value is zero
42
- #
43
- # @example
44
- # Money.ca_dollar(10000).format(:no_cents_if_whole => true) #=> "$100"
45
- # Money.ca_dollar(10034).format(:no_cents_if_whole => true) #=> "$100.34"
46
- #
47
- # @option rules [Boolean, String, nil] :symbol (true) Whether a money symbol
48
- # should be prepended to the result string. The default is true. This method
49
- # attempts to pick a symbol that's suitable for the given currency.
50
- #
51
- # @example
52
- # Money.new(100, "USD") #=> "$1.00"
53
- # Money.new(100, "GBP") #=> "£1.00"
54
- # Money.new(100, "EUR") #=> "€1.00"
55
- #
56
- # # Same thing.
57
- # Money.new(100, "USD").format(:symbol => true) #=> "$1.00"
58
- # Money.new(100, "GBP").format(:symbol => true) #=> "£1.00"
59
- # Money.new(100, "EUR").format(:symbol => true) #=> "€1.00"
60
- #
61
- # # You can specify a false expression or an empty string to disable
62
- # # prepending a money symbol.§
63
- # Money.new(100, "USD").format(:symbol => false) #=> "1.00"
64
- # Money.new(100, "GBP").format(:symbol => nil) #=> "1.00"
65
- # Money.new(100, "EUR").format(:symbol => "") #=> "1.00"
66
- #
67
- # # If the symbol for the given currency isn't known, then it will default
68
- # # to "¤" as symbol.
69
- # Money.new(100, "AWG").format(:symbol => true) #=> "¤1.00"
70
- #
71
- # # You can specify a string as value to enforce using a particular symbol.
72
- # Money.new(100, "AWG").format(:symbol => "ƒ") #=> "ƒ1.00"
73
- #
74
- # # You can specify a indian currency format
75
- # Money.new(10000000, "INR").format(:south_asian_number_formatting => true) #=> "1,00,000.00"
76
- # Money.new(10000000).format(:south_asian_number_formatting => true) #=> "$1,00,000.00"
77
- #
78
- # @option rules [Boolean, nil] :symbol_before_without_space (true) Whether
79
- # a space between the money symbol and the amount should be inserted when
80
- # +:symbol_position+ is +:before+. The default is true (meaning no space). Ignored
81
- # if +:symbol+ is false or +:symbol_position+ is not +:before+.
82
- #
83
- # @example
84
- # # Default is to not insert a space.
85
- # Money.new(100, "USD").format #=> "$1.00"
86
- #
87
- # # Same thing.
88
- # Money.new(100, "USD").format(:symbol_before_without_space => true) #=> "$1.00"
89
- #
90
- # # If set to false, will insert a space.
91
- # Money.new(100, "USD").format(:symbol_before_without_space => false) #=> "$ 1.00"
92
- #
93
- # @option rules [Boolean, nil] :symbol_after_without_space (false) Whether
94
- # a space between the amount and the money symbol should be inserted when
95
- # +:symbol_position+ is +:after+. The default is false (meaning space). Ignored
96
- # if +:symbol+ is false or +:symbol_position+ is not +:after+.
97
- #
98
- # @example
99
- # # Default is to insert a space.
100
- # Money.new(100, "USD").format(:symbol_position => :after) #=> "1.00 $"
101
- #
102
- # # If set to true, will not insert a space.
103
- # Money.new(100, "USD").format(:symbol_position => :after, :symbol_after_without_space => true) #=> "1.00$"
104
- #
105
- # @option rules [Boolean, String, nil] :decimal_mark (true) Whether the
106
- # currency should be separated by the specified character or '.'
107
- #
108
- # @example
109
- # # If a string is specified, it's value is used.
110
- # Money.new(100, "USD").format(:decimal_mark => ",") #=> "$1,00"
111
- #
112
- # # If the decimal_mark for a given currency isn't known, then it will default
113
- # # to "." as decimal_mark.
114
- # Money.new(100, "FOO").format #=> "$1.00"
115
- #
116
- # @option rules [Boolean, String, nil] :thousands_separator (true) Whether
117
- # the currency should be delimited by the specified character or ','
118
- #
119
- # @example
120
- # # If false is specified, no thousands_separator is used.
121
- # Money.new(100000, "USD").format(:thousands_separator => false) #=> "1000.00"
122
- # Money.new(100000, "USD").format(:thousands_separator => nil) #=> "1000.00"
123
- # Money.new(100000, "USD").format(:thousands_separator => "") #=> "1000.00"
124
- #
125
- # # If a string is specified, it's value is used.
126
- # Money.new(100000, "USD").format(:thousands_separator => ".") #=> "$1.000.00"
127
- #
128
- # # If the thousands_separator for a given currency isn't known, then it will
129
- # # default to "," as thousands_separator.
130
- # Money.new(100000, "FOO").format #=> "$1,000.00"
131
- #
132
- # @option rules [Boolean] :html (false) Whether the currency should be
133
- # HTML-formatted. Only useful in combination with +:with_currency+.
134
- #
135
- # @example
136
- # Money.ca_dollar(570).format(:html => true, :with_currency => true)
137
- # #=> "$5.70 <span class=\"currency\">CAD</span>"
138
- #
139
- # @option rules [Boolean] :sign_before_symbol (false) Whether the sign should be
140
- # before the currency symbol.
141
- #
142
- # @example
143
- # # You can specify to display the sign before the symbol for negative numbers
144
- # Money.new(-100, "GBP").format(:sign_before_symbol => true) #=> "-£1.00"
145
- # Money.new(-100, "GBP").format(:sign_before_symbol => false) #=> "£-1.00"
146
- # Money.new(-100, "GBP").format #=> "£-1.00"
147
- #
148
- # @option rules [Boolean] :sign_positive (false) Whether positive numbers should be
149
- # signed, too.
150
- #
151
- # @example
152
- # # You can specify to display the sign with positive numbers
153
- # Money.new(100, "GBP").format(:sign_positive => true, :sign_before_symbol => true) #=> "+£1.00"
154
- # Money.new(100, "GBP").format(:sign_positive => true, :sign_before_symbol => false) #=> "£+1.00"
155
- # Money.new(100, "GBP").format(:sign_positive => false, :sign_before_symbol => true) #=> "£1.00"
156
- # Money.new(100, "GBP").format(:sign_positive => false, :sign_before_symbol => false) #=> "£1.00"
157
- # Money.new(100, "GBP").format #=> "£+1.00"
158
- #
159
- # @option rules [Boolean] :disambiguate (false) Prevents the result from being ambiguous
160
- # due to equal symbols for different currencies. Uses the `disambiguate_symbol`.
161
- #
162
- # @example
163
- # Money.new(10000, "USD").format(:disambiguate => false) #=> "$100.00"
164
- # Money.new(10000, "CAD").format(:disambiguate => false) #=> "$100.00"
165
- # Money.new(10000, "USD").format(:disambiguate => true) #=> "$100.00"
166
- # Money.new(10000, "CAD").format(:disambiguate => true) #=> "C$100.00"
167
- #
168
- # @option rules [Boolean] :html_wrap_symbol (false) Wraps the currency symbol
169
- # in a html <span> tag.
170
- #
171
- # @example
172
- # Money.new(10000, "USD").format(:disambiguate => false)
173
- # #=> "<span class=\"currency_symbol\">$100.00</span>
174
- #
175
- # @option rules [Symbol] :symbol_position (:before) `:before` if the currency
176
- # symbol goes before the amount, `:after` if it goes after.
177
- #
178
- # @example
179
- # Money.new(10000, "USD").format(:symbol_position => :before) #=> "$100.00"
180
- # Money.new(10000, "USD").format(:symbol_position => :after) #=> "100.00 $"
181
- #
182
- # @option rules [Boolean] :translate (true) `true` Checks for custom
183
- # symbol definitions using I18n.
184
- #
185
- # @example
186
- # # With the following entry in the translation files:
187
- # # en:
188
- # # number:
189
- # # currency:
190
- # # symbol:
191
- # # CAD: "CAD$"
192
- # Money.new(10000, "CAD").format(:translate => true) #=> "CAD$100.00"
193
- #
194
- # @example
195
- # Money.new(89000, :btc).format(:drop_trailing_zeros => true) #=> B⃦0.00089
196
- # Money.new(110, :usd).format(:drop_trailing_zeros => true) #=> $1.1
197
- #
198
- # Note that the default rules can be defined through {Money.default_formatting_rules} hash.
199
- #
200
- # @see Money.default_formatting_rules Money.default_formatting_rules for more information.
201
- def format(*rules)
202
- # support for old format parameters
203
- rules = normalize_formatting_rules(rules)
204
-
205
- rules = default_formatting_rules.merge(rules)
206
- rules = localize_formatting_rules(rules)
207
- rules = translate_formatting_rules(rules) if rules[:translate]
208
-
209
- thousands_separator = self.thousands_separator
210
- decimal_mark = self.decimal_mark
211
-
212
- escaped_decimal_mark = Regexp.escape(decimal_mark)
213
-
214
- if fractional == 0
215
- if rules[:display_free].respond_to?(:to_str)
216
- return rules[:display_free]
217
- elsif rules[:display_free]
218
- return "free"
219
- end
220
- end
221
-
222
- symbol_value = symbol_value_from(rules)
223
-
224
- formatted = self.abs.to_s
225
-
226
- if rules[:rounded_infinite_precision]
227
- formatted.gsub!(/#{decimal_mark}/, '.') unless '.' == decimal_mark
228
- formatted = ((BigDecimal(formatted) * currency.subunit_to_unit).round / BigDecimal(currency.subunit_to_unit.to_s)).to_s("F")
229
- formatted.gsub!(/\..*/) do |decimal_part|
230
- decimal_part << '0' while decimal_part.length < (currency.decimal_places + 1)
231
- decimal_part
232
- end
233
- formatted.gsub!(/\./, decimal_mark) unless '.' == decimal_mark
234
- end
235
-
236
- sign = self.negative? ? '-' : ''
237
-
238
- if rules[:no_cents] || (rules[:no_cents_if_whole] && cents % currency.subunit_to_unit == 0)
239
- formatted = "#{formatted.to_i}"
240
- end
241
-
242
- # Inspiration: https://github.com/rails/rails/blob/16214d1108c31174c94503caced3855b0f6bad95/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb#L72-L79
243
- if rules[:drop_trailing_zeros]
244
- formatted = formatted.sub(/(#{escaped_decimal_mark})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_decimal_mark}\z/, '')
245
- end
246
- has_decimal_value = !!(formatted =~ /#{escaped_decimal_mark}/)
247
-
248
- thousands_separator_value = thousands_separator
249
- # Determine thousands_separator
250
- if rules.has_key?(:thousands_separator)
251
- thousands_separator_value = rules[:thousands_separator] || ''
252
- end
253
-
254
- # Apply thousands_separator
255
- formatted.gsub!(regexp_format(formatted, rules, decimal_mark, symbol_value),
256
- "\\1#{thousands_separator_value}")
257
-
258
- symbol_position = symbol_position_from(rules)
259
-
260
- if rules[:sign_positive] == true && self.positive?
261
- sign = '+'
262
- end
263
-
264
- if rules[:sign_before_symbol] == true
265
- sign_before = sign
266
- sign = ''
267
- end
268
-
269
- if symbol_value && !symbol_value.empty?
270
- symbol_value = "<span class=\"currency_symbol\">#{symbol_value}</span>" if rules[:html_wrap_symbol]
271
-
272
- formatted = if symbol_position == :before
273
- symbol_space = rules[:symbol_before_without_space] === false ? " " : ""
274
- "#{sign_before}#{symbol_value}#{symbol_space}#{sign}#{formatted}"
275
- else
276
- symbol_space = rules[:symbol_after_without_space] ? "" : " "
277
- "#{sign_before}#{sign}#{formatted}#{symbol_space}#{symbol_value}"
278
- end
279
- else
280
- formatted="#{sign_before}#{sign}#{formatted}"
281
- end
282
-
283
- apply_decimal_mark_from_rules(formatted, rules) if has_decimal_value
284
-
285
- if rules[:with_currency]
286
- formatted << " "
287
- formatted << '<span class="currency">' if rules[:html]
288
- formatted << currency.to_s
289
- formatted << '</span>' if rules[:html]
290
- end
291
- formatted
292
- end
293
-
294
- def thousands_separator
295
- i18n_format_for(:thousands_separator, :delimiter, ",")
296
- end
297
-
298
- def decimal_mark
299
- i18n_format_for(:decimal_mark, :separator, ".")
300
- end
301
-
302
- alias_method :delimiter, :thousands_separator
303
- alias_method :separator, :decimal_mark
304
-
305
- private
306
-
307
- def i18n_format_for(method, name, character)
308
- if self.class.use_i18n
309
- begin
310
- I18n.t name, :scope => "number.currency.format", :raise => true
311
- rescue I18n::MissingTranslationData
312
- I18n.t name, :scope =>"number.format", :default => (currency.send(method) || character)
313
- end
314
- else
315
- currency.send(method) || character
316
- end
317
- end
318
-
319
- # Cleans up formatting rules.
320
- #
321
- # @param [Hash] rules
322
- #
323
- # @return [Hash]
324
- def normalize_formatting_rules(rules)
325
- if rules.size == 0
326
- rules = {}
327
- elsif rules.size == 1
328
- rules = rules.pop
329
- rules = { rules => true } if rules.is_a?(Symbol)
330
- end
331
- if !rules.include?(:decimal_mark) && rules.include?(:separator)
332
- rules[:decimal_mark] = rules[:separator]
333
- end
334
- if !rules.include?(:thousands_separator) && rules.include?(:delimiter)
335
- rules[:thousands_separator] = rules[:delimiter]
336
- end
337
- rules
338
- end
339
-
340
- # Applies decimal mark from rules to formatted
341
- #
342
- # @param [String] formatted
343
- # @param [Hash] rules
344
- def apply_decimal_mark_from_rules(formatted, rules)
345
- if rules.has_key?(:decimal_mark) && rules[:decimal_mark] &&
346
- rules[:decimal_mark] != decimal_mark
347
-
348
- regexp_decimal = Regexp.escape(decimal_mark)
349
- formatted.sub!(/(.*)(#{regexp_decimal})(.*)\Z/,
350
- "\\1#{rules[:decimal_mark]}\\3")
351
- end
352
- end
353
- end
354
-
355
- def default_formatting_rules
356
- self.class.default_formatting_rules || {}
357
- end
358
-
359
- def regexp_format(formatted, rules, decimal_mark, symbol_value)
360
- regexp_decimal = Regexp.escape(decimal_mark)
361
- if rules[:south_asian_number_formatting]
362
- /(\d+?)(?=(\d\d)+(\d)(?:\.))/
363
- else
364
- # Symbols may contain decimal marks (E.g "դր.")
365
- if formatted.sub(symbol_value.to_s, "") =~ /#{regexp_decimal}/
366
- /(\d)(?=(?:\d{3})+(?:#{regexp_decimal}))/
367
- else
368
- /(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/
369
- end
370
- end
371
- end
372
-
373
- def translate_formatting_rules(rules)
374
- begin
375
- rules[:symbol] = I18n.t currency.iso_code, :scope => "number.currency.symbol", :raise => true
376
- rescue I18n::MissingTranslationData
377
- # Do nothing
378
- end
379
- rules
380
- end
381
-
382
- def localize_formatting_rules(rules)
383
- if currency.iso_code == "JPY" && I18n.locale == :ja
384
- rules[:symbol] = "円" unless rules[:symbol] == false
385
- rules[:symbol_position] = :after
386
- rules[:symbol_after_without_space] = true
387
- end
388
- rules
389
- end
390
-
391
- def symbol_value_from(rules)
392
- if rules.has_key?(:symbol)
393
- if rules[:symbol] === true
394
- if rules[:disambiguate] && currency.disambiguate_symbol
395
- currency.disambiguate_symbol
396
- else
397
- symbol
398
- end
399
- elsif rules[:symbol]
400
- rules[:symbol]
401
- else
402
- ""
403
- end
404
- elsif rules[:html]
405
- currency.html_entity == '' ? currency.symbol : currency.html_entity
406
- elsif rules[:disambiguate] && currency.disambiguate_symbol
407
- currency.disambiguate_symbol
408
- else
409
- symbol
410
- end
411
- end
412
-
413
- def symbol_position_from(rules)
414
- if rules.has_key?(:symbol_position)
415
- if [:before, :after].include?(rules[:symbol_position])
416
- return rules[:symbol_position]
417
- else
418
- raise ArgumentError, ":symbol_position must be ':before' or ':after'"
419
- end
420
- elsif currency.symbol_first?
421
- :before
422
- else
423
- :after
424
- end
425
- end
426
- end
@@ -1,79 +0,0 @@
1
- class Money
2
- module Bank
3
- describe Base do
4
-
5
- describe ".instance" do
6
- it "is local to one class" do
7
- klass = Base
8
- subclass = Class.new(Base)
9
- expect(klass.instance).not_to eq subclass.instance
10
- end
11
- end
12
-
13
- describe "#initialize" do
14
- it "accepts a block and stores @rounding_method" do
15
- proc = Proc.new { |n| n.ceil }
16
- bank = Base.new(&proc)
17
- expect(bank.rounding_method).to eq proc
18
- end
19
- end
20
-
21
- describe "#setup" do
22
- it "calls #setup after #initialize" do
23
- class MyBank < Base
24
- attr_reader :setup_called
25
-
26
- def setup
27
- @setup_called = true
28
- end
29
- end
30
-
31
- bank = MyBank.new
32
- expect(bank.setup_called).to eq true
33
- end
34
- end
35
-
36
- describe "#exchange_with" do
37
- it "is not implemented" do
38
- expect { subject.exchange_with(Money.new(100, 'USD'), 'EUR') }.to raise_exception(NotImplementedError)
39
- end
40
- end
41
-
42
- describe "#same_currency?" do
43
- it "accepts str/str" do
44
- expect { subject.send(:same_currency?, 'USD', 'EUR') }.to_not raise_exception
45
- end
46
-
47
- it "accepts currency/str" do
48
- expect { subject.send(:same_currency?, Currency.wrap('USD'), 'EUR') }.to_not raise_exception
49
- end
50
-
51
- it "accepts str/currency" do
52
- expect { subject.send(:same_currency?, 'USD', Currency.wrap('EUR')) }.to_not raise_exception
53
- end
54
-
55
- it "accepts currency/currency" do
56
- expect { subject.send(:same_currency?, Currency.wrap('USD'), Currency.wrap('EUR')) }.to_not raise_exception
57
- end
58
-
59
- it "returns true when currencies match" do
60
- expect(subject.send(:same_currency?, 'USD', 'USD')).to be true
61
- expect(subject.send(:same_currency?, Currency.wrap('USD'), 'USD')).to be true
62
- expect(subject.send(:same_currency?, 'USD', Currency.wrap('USD'))).to be true
63
- expect(subject.send(:same_currency?, Currency.wrap('USD'), Currency.wrap('USD'))).to be true
64
- end
65
-
66
- it "returns false when currencies do not match" do
67
- expect(subject.send(:same_currency?, 'USD', 'EUR')).to be false
68
- expect(subject.send(:same_currency?, Currency.wrap('USD'), 'EUR')).to be false
69
- expect(subject.send(:same_currency?, 'USD', Currency.wrap('EUR'))).to be false
70
- expect(subject.send(:same_currency?, Currency.wrap('USD'), Currency.wrap('EUR'))).to be false
71
- end
72
-
73
- it "raises an UnknownCurrency exception when an unknown currency is passed" do
74
- expect { subject.send(:same_currency?, 'AAA', 'BBB') }.to raise_exception(Currency::UnknownCurrency)
75
- end
76
- end
77
- end
78
- end
79
- end
@@ -1,13 +0,0 @@
1
- class Money
2
- module Bank
3
- describe SingleCurrency do
4
- describe "#exchange_with" do
5
- it "raises when called" do
6
- expect {
7
- subject.exchange_with(Money.new(100, 'USD'), 'EUR')
8
- }.to raise_exception(DifferentCurrencyError, "No exchanging of currencies allowed: 1.00 USD to EUR")
9
- end
10
- end
11
- end
12
- end
13
- end