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