money 6.7.1 → 6.13.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 +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