money 6.7.0 → 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.
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
@@ -1,417 +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 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
- # s = Money.ca_dollar(570).format(:html => true, :with_currency => true)
137
- # s #=> "$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+ 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
- if fractional == 0
210
- if rules[:display_free].respond_to?(:to_str)
211
- return rules[:display_free]
212
- elsif rules[:display_free]
213
- return "free"
214
- end
215
- end
216
-
217
- symbol_value = symbol_value_from(rules)
218
-
219
- formatted = self.abs.to_s
220
-
221
- if rules[:rounded_infinite_precision]
222
- formatted.gsub!(/#{decimal_mark}/, '.') unless '.' == decimal_mark
223
- formatted = ((BigDecimal(formatted) * currency.subunit_to_unit).round / BigDecimal(currency.subunit_to_unit.to_s)).to_s("F")
224
- formatted.gsub!(/\..*/) do |decimal_part|
225
- decimal_part << '0' while decimal_part.length < (currency.decimal_places + 1)
226
- decimal_part
227
- end
228
- formatted.gsub!(/\./, decimal_mark) unless '.' == decimal_mark
229
- end
230
-
231
- sign = self.negative? ? '-' : ''
232
-
233
- if rules[:no_cents] || (rules[:no_cents_if_whole] && cents % currency.subunit_to_unit == 0)
234
- formatted = "#{formatted.to_i}"
235
- end
236
-
237
- # Inspiration: https://github.com/rails/rails/blob/16214d1108c31174c94503caced3855b0f6bad95/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb#L72-L79
238
- if rules[:drop_trailing_zeros]
239
- escaped_decimal_mark = Regexp.escape(decimal_mark)
240
- formatted = formatted.sub(/(#{escaped_decimal_mark})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_decimal_mark}\z/, '')
241
- end
242
-
243
- thousands_separator_value = thousands_separator
244
- # Determine thousands_separator
245
- if rules.has_key?(:thousands_separator)
246
- thousands_separator_value = rules[:thousands_separator] || ''
247
- end
248
-
249
- # Apply thousands_separator
250
- formatted.gsub!(regexp_format(formatted, rules, decimal_mark, symbol_value),
251
- "\\1#{thousands_separator_value}")
252
-
253
- symbol_position = symbol_position_from(rules)
254
-
255
- if rules[:sign_positive] == true && self.positive?
256
- sign = '+'
257
- end
258
-
259
- if rules[:sign_before_symbol] == true
260
- sign_before = sign
261
- sign = ''
262
- end
263
-
264
- if symbol_value && !symbol_value.empty?
265
- symbol_value = "<span class=\"currency_symbol\">#{symbol_value}</span>" if rules[:html_wrap_symbol]
266
-
267
- formatted = if symbol_position == :before
268
- symbol_space = rules[:symbol_before_without_space] === false ? " " : ""
269
- "#{sign_before}#{symbol_value}#{symbol_space}#{sign}#{formatted}"
270
- else
271
- symbol_space = rules[:symbol_after_without_space] ? "" : " "
272
- "#{sign_before}#{sign}#{formatted}#{symbol_space}#{symbol_value}"
273
- end
274
- else
275
- formatted="#{sign_before}#{sign}#{formatted}"
276
- end
277
-
278
- apply_decimal_mark_from_rules(formatted, rules)
279
-
280
- if rules[:with_currency]
281
- formatted << " "
282
- formatted << '<span class="currency">' if rules[:html]
283
- formatted << currency.to_s
284
- formatted << '</span>' if rules[:html]
285
- end
286
- formatted
287
- end
288
-
289
- def thousands_separator
290
- i18n_format_for(:thousands_separator, :delimiter, ",")
291
- end
292
-
293
- def decimal_mark
294
- i18n_format_for(:decimal_mark, :separator, ".")
295
- end
296
-
297
- alias_method :delimiter, :thousands_separator
298
- alias_method :separator, :decimal_mark
299
-
300
- private
301
-
302
- def i18n_format_for(method, name, character)
303
- if self.class.use_i18n
304
- begin
305
- I18n.t name, :scope => "number.currency.format", :raise => true
306
- rescue I18n::MissingTranslationData
307
- I18n.t name, :scope =>"number.format", :default => (currency.send(method) || character)
308
- end
309
- else
310
- currency.send(method) || character
311
- end
312
- end
313
-
314
- # Cleans up formatting rules.
315
- #
316
- # @param [Hash] rules
317
- #
318
- # @return [Hash]
319
- def normalize_formatting_rules(rules)
320
- if rules.size == 0
321
- rules = {}
322
- elsif rules.size == 1
323
- rules = rules.pop
324
- rules = { rules => true } if rules.is_a?(Symbol)
325
- end
326
- if !rules.include?(:decimal_mark) && rules.include?(:separator)
327
- rules[:decimal_mark] = rules[:separator]
328
- end
329
- if !rules.include?(:thousands_separator) && rules.include?(:delimiter)
330
- rules[:thousands_separator] = rules[:delimiter]
331
- end
332
- rules
333
- end
334
-
335
- # Applies decimal mark from rules to formatted
336
- #
337
- # @param [String] formatted
338
- # @param [Hash] rules
339
- def apply_decimal_mark_from_rules(formatted, rules)
340
- if rules.has_key?(:decimal_mark) && rules[:decimal_mark] &&
341
- rules[:decimal_mark] != decimal_mark
342
-
343
- regexp_decimal = Regexp.escape(decimal_mark)
344
- formatted.sub!(/(.*)(#{regexp_decimal})(.*)\Z/,
345
- "\\1#{rules[:decimal_mark]}\\3")
346
- end
347
- end
348
- end
349
-
350
- def default_formatting_rules
351
- self.class.default_formatting_rules || {}
352
- end
353
-
354
- def regexp_format(formatted, rules, decimal_mark, symbol_value)
355
- regexp_decimal = Regexp.escape(decimal_mark)
356
- if rules[:south_asian_number_formatting]
357
- /(\d+?)(?=(\d\d)+(\d)(?:\.))/
358
- else
359
- # Symbols may contain decimal marks (E.g "դր.")
360
- if formatted.sub(symbol_value.to_s, "") =~ /#{regexp_decimal}/
361
- /(\d)(?=(?:\d{3})+(?:#{regexp_decimal}))/
362
- else
363
- /(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/
364
- end
365
- end
366
- end
367
-
368
- def translate_formatting_rules(rules)
369
- begin
370
- rules[:symbol] = I18n.t currency.iso_code, :scope => "number.currency.symbol", :raise => true
371
- rescue I18n::MissingTranslationData
372
- # Do nothing
373
- end
374
- rules
375
- end
376
-
377
- def localize_formatting_rules(rules)
378
- if currency.iso_code == "JPY" && I18n.locale == :ja
379
- rules[:symbol] = "円" unless rules[:symbol] == false
380
- rules[:symbol_position] = :after
381
- rules[:symbol_after_without_space] = true
382
- end
383
- rules
384
- end
385
-
386
- def symbol_value_from(rules)
387
- if rules.has_key?(:symbol)
388
- if rules[:symbol] === true
389
- symbol
390
- elsif rules[:symbol]
391
- rules[:symbol]
392
- else
393
- ""
394
- end
395
- elsif rules[:html]
396
- currency.html_entity == '' ? currency.symbol : currency.html_entity
397
- elsif rules[:disambiguate] and currency.disambiguate_symbol
398
- currency.disambiguate_symbol
399
- else
400
- symbol
401
- end
402
- end
403
-
404
- def symbol_position_from(rules)
405
- if rules.has_key?(:symbol_position)
406
- if [:before, :after].include?(rules[:symbol_position])
407
- return rules[:symbol_position]
408
- else
409
- raise ArgumentError, ":symbol_position must be ':before' or ':after'"
410
- end
411
- elsif currency.symbol_first?
412
- :before
413
- else
414
- :after
415
- end
416
- end
417
- end