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
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+
3
+ class Money
4
+ class Allocation
5
+ # Splits a given amount in parts without loosing pennies.
6
+ # The left-over pennies will be distributed round-robin amongst the parties. This means that
7
+ # parties listed first will likely receive more pennies than ones that are 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 = parts.is_a?(Numeric) ? Array.new(parts, 1) : parts.dup
17
+
18
+ raise ArgumentError, 'need at least one party' if parts.empty?
19
+
20
+ result = []
21
+ remaining_amount = amount
22
+
23
+ until parts.empty? do
24
+ parts_sum = parts.inject(0, :+)
25
+ part = parts.pop
26
+
27
+ current_split = remaining_amount * part / parts_sum
28
+ current_split = current_split.truncate if whole_amounts
29
+
30
+ result.unshift current_split
31
+ remaining_amount -= current_split
32
+ end
33
+
34
+ result
35
+ end
36
+ end
37
+ end
@@ -1,18 +1,13 @@
1
1
  class Money
2
- CoercedNumber = Struct.new(:value) do
3
- include Comparable
4
-
5
- def +(other) raise TypeError; end
6
- def -(other) raise TypeError; end
7
- def /(other) raise TypeError; end
8
- def <=>(other) raise TypeError; end
9
-
10
- def *(other)
11
- other * value
12
- end
13
- end
14
-
15
2
  module Arithmetic
3
+ # Wrapper for coerced numeric values to distinguish
4
+ # when numeric was on the 1st place in operation.
5
+ CoercedNumeric = Struct.new(:value) do
6
+ # Proxy #zero? method to skip unnecessary typecasts. See #- and #+.
7
+ def zero?
8
+ value.zero?
9
+ end
10
+ end
16
11
 
17
12
  # Returns a money object with changed polarity.
18
13
  #
@@ -21,7 +16,7 @@ class Money
21
16
  # @example
22
17
  # - Money.new(100) #=> #<Money @fractional=-100>
23
18
  def -@
24
- self.class.new(-fractional, currency)
19
+ self.class.new(-fractional, currency, bank)
25
20
  end
26
21
 
27
22
  # Checks whether two Money objects have the same currency and the same
@@ -53,18 +48,28 @@ class Money
53
48
  #
54
49
  # @param [Money] other_money Value to compare with.
55
50
  #
56
- # @return [Fixnum]
51
+ # @return [Integer]
57
52
  #
58
53
  # @raise [TypeError] when other object is not Money
59
54
  #
60
- def <=>(other_money)
61
- return nil unless other_money.is_a?(Money)
62
- if fractional != 0 && other_money.fractional != 0 && currency != other_money.currency
63
- other_money = other_money.exchange_to(currency)
55
+ def <=>(other)
56
+ unless other.is_a?(Money)
57
+ return unless other.respond_to?(:zero?) && other.zero?
58
+ return other.is_a?(CoercedNumeric) ? 0 <=> fractional : fractional <=> 0
64
59
  end
65
- fractional <=> other_money.fractional
60
+ return 0 if zero? && other.zero?
61
+ other = other.exchange_to(currency)
62
+ fractional <=> other.fractional
66
63
  rescue Money::Bank::UnknownRate
67
- nil
64
+ end
65
+
66
+ # Uses Comparable's implementation but raises ArgumentError if non-zero
67
+ # numeric value is given.
68
+ def ==(other)
69
+ if other.is_a?(Numeric) && !other.zero?
70
+ raise ArgumentError, 'Money#== supports only zero numerics'
71
+ end
72
+ super
68
73
  end
69
74
 
70
75
  # Test if the amount is positive. Returns +true+ if the money amount is
@@ -93,6 +98,7 @@ class Money
93
98
  fractional < 0
94
99
  end
95
100
 
101
+ # @method +(other)
96
102
  # Returns a new Money object containing the sum of the two operands' monetary
97
103
  # values. If +other_money+ has a different currency then its monetary value
98
104
  # is automatically exchanged to this object's currency using +exchange_to+.
@@ -103,13 +109,8 @@ class Money
103
109
  #
104
110
  # @example
105
111
  # Money.new(100) + Money.new(100) #=> #<Money @fractional=200>
106
- def +(other_money)
107
- return self if other_money.zero?
108
- raise TypeError unless other_money.is_a?(Money)
109
- other_money = other_money.exchange_to(currency)
110
- self.class.new(fractional + other_money.fractional, currency)
111
- end
112
-
112
+ #
113
+ # @method -(other)
113
114
  # Returns a new Money object containing the difference between the two
114
115
  # operands' monetary values. If +other_money+ has a different currency then
115
116
  # its monetary value is automatically exchanged to this object's currency
@@ -121,11 +122,17 @@ class Money
121
122
  #
122
123
  # @example
123
124
  # Money.new(100) - Money.new(99) #=> #<Money @fractional=1>
124
- def -(other_money)
125
- return self if other_money.zero?
126
- raise TypeError unless other_money.is_a?(Money)
127
- other_money = other_money.exchange_to(currency)
128
- self.class.new(fractional - other_money.fractional, currency)
125
+ [:+, :-].each do |op|
126
+ define_method(op) do |other|
127
+ unless other.is_a?(Money)
128
+ if other.zero?
129
+ return other.is_a?(CoercedNumeric) ? Money.empty(currency).public_send(op, self) : self
130
+ end
131
+ raise TypeError
132
+ end
133
+ other = other.exchange_to(currency)
134
+ self.class.new(fractional.public_send(op, other.fractional), currency, bank)
135
+ end
129
136
  end
130
137
 
131
138
  # Multiplies the monetary value with the given number and returns a new
@@ -137,16 +144,17 @@ class Money
137
144
  #
138
145
  # @return [Money] The resulting money.
139
146
  #
140
- # @raise [ArgumentError] If +value+ is NOT a number.
147
+ # @raise [TypeError] If +value+ is NOT a number.
141
148
  #
142
149
  # @example
143
150
  # Money.new(100) * 2 #=> #<Money @fractional=200>
144
151
  #
145
152
  def *(value)
153
+ value = value.value if value.is_a?(CoercedNumeric)
146
154
  if value.is_a? Numeric
147
- self.class.new(fractional * value, currency)
155
+ self.class.new(fractional * value, currency, bank)
148
156
  else
149
- raise ArgumentError, "Can't multiply a #{self.class.name} by a #{value.class.name}'s value"
157
+ raise TypeError, "Can't multiply a #{self.class.name} by a #{value.class.name}'s value"
150
158
  end
151
159
  end
152
160
 
@@ -169,7 +177,8 @@ class Money
169
177
  if value.is_a?(self.class)
170
178
  fractional / as_d(value.exchange_to(currency).fractional).to_f
171
179
  else
172
- self.class.new(fractional / as_d(value), currency)
180
+ raise TypeError, 'Can not divide by Money' if value.is_a?(CoercedNumeric)
181
+ self.class.new(fractional / as_d(value), currency, bank)
173
182
  end
174
183
  end
175
184
 
@@ -189,9 +198,9 @@ class Money
189
198
  # Divide money by money or fixnum and return array containing quotient and
190
199
  # modulus.
191
200
  #
192
- # @param [Money, Fixnum] val Number to divmod by.
201
+ # @param [Money, Integer] val Number to divmod by.
193
202
  #
194
- # @return [Array<Money,Money>,Array<Fixnum,Money>]
203
+ # @return [Array<Money,Money>,Array<Integer,Money>]
195
204
  #
196
205
  # @example
197
206
  # Money.new(100).divmod(9) #=> [#<Money @fractional=11>, #<Money @fractional=1>]
@@ -207,23 +216,19 @@ class Money
207
216
  def divmod_money(val)
208
217
  cents = val.exchange_to(currency).cents
209
218
  quotient, remainder = fractional.divmod(cents)
210
- [quotient, self.class.new(remainder, currency)]
219
+ [quotient, self.class.new(remainder, currency, bank)]
211
220
  end
212
221
  private :divmod_money
213
222
 
214
223
  def divmod_other(val)
215
- if self.class.infinite_precision
216
- quotient, remainder = fractional.divmod(as_d(val))
217
- [self.class.new(quotient, currency), self.class.new(remainder, currency)]
218
- else
219
- [div(val), self.class.new(fractional.modulo(val), currency)]
220
- end
224
+ quotient, remainder = fractional.divmod(as_d(val))
225
+ [self.class.new(quotient, currency, bank), self.class.new(remainder, currency, bank)]
221
226
  end
222
227
  private :divmod_other
223
228
 
224
229
  # Equivalent to +self.divmod(val)[1]+
225
230
  #
226
- # @param [Money, Fixnum] val Number take modulo with.
231
+ # @param [Money, Integer] val Number take modulo with.
227
232
  #
228
233
  # @return [Money]
229
234
  #
@@ -236,7 +241,7 @@ class Money
236
241
 
237
242
  # Synonym for +#modulo+.
238
243
  #
239
- # @param [Money, Fixnum] val Number take modulo with.
244
+ # @param [Money, Integer] val Number take modulo with.
240
245
  #
241
246
  # @return [Money]
242
247
  #
@@ -247,7 +252,7 @@ class Money
247
252
 
248
253
  # If different signs +self.modulo(val) - val+ otherwise +self.modulo(val)+
249
254
  #
250
- # @param [Money, Fixnum] val Number to rake remainder with.
255
+ # @param [Money, Integer] val Number to rake remainder with.
251
256
  #
252
257
  # @return [Money]
253
258
  #
@@ -261,7 +266,7 @@ class Money
261
266
  if (fractional < 0 && val < 0) || (fractional > 0 && val > 0)
262
267
  self.modulo(val)
263
268
  else
264
- self.modulo(val) - (val.is_a?(Money) ? val : self.class.new(val, currency))
269
+ self.modulo(val) - (val.is_a?(Money) ? val : self.class.new(val, currency, bank))
265
270
  end
266
271
  end
267
272
 
@@ -272,7 +277,7 @@ class Money
272
277
  # @example
273
278
  # Money.new(-100).abs #=> #<Money @fractional=100>
274
279
  def abs
275
- self.class.new(fractional.abs, currency)
280
+ self.class.new(fractional.abs, currency, bank)
276
281
  end
277
282
 
278
283
  # Test if the money amount is zero.
@@ -304,7 +309,7 @@ class Money
304
309
  # @example
305
310
  # 2 * Money.new(10) #=> #<Money @fractional=20>
306
311
  def coerce(other)
307
- [CoercedNumber.new(other), self]
312
+ [self, CoercedNumeric.new(other)]
308
313
  end
309
314
  end
310
315
  end
@@ -10,8 +10,7 @@ class Money
10
10
  # @example
11
11
  # Money.empty #=> #<Money @fractional=0>
12
12
  def empty(currency = default_currency)
13
- @empty ||= {}
14
- @empty[currency] ||= new(0, currency).freeze
13
+ new(0, currency)
15
14
  end
16
15
  alias_method :zero, :empty
17
16
 
@@ -0,0 +1,397 @@
1
+ # encoding: UTF-8
2
+ require 'money/money/formatting_rules'
3
+
4
+ class Money
5
+ class Formatter
6
+ DEFAULTS = {
7
+ thousands_separator: '',
8
+ decimal_mark: '.'
9
+ }.freeze
10
+
11
+ # Creates a formatted price string according to several rules.
12
+ #
13
+ # @param [Hash] rules The options used to format the string.
14
+ #
15
+ # @return [String]
16
+ #
17
+ # @option rules [Boolean, String] :display_free (false) Whether a zero
18
+ # amount of money should be formatted of "free" or as the supplied string.
19
+ #
20
+ # @example
21
+ # Money.us_dollar(0).format(display_free: true) #=> "free"
22
+ # Money.us_dollar(0).format(display_free: "gratis") #=> "gratis"
23
+ # Money.us_dollar(0).format #=> "$0.00"
24
+ #
25
+ # @option rules [Boolean] :with_currency (false) Whether the currency name
26
+ # should be appended to the result string.
27
+ #
28
+ # @example
29
+ # Money.ca_dollar(100).format #=> "$1.00"
30
+ # Money.ca_dollar(100).format(with_currency: true) #=> "$1.00 CAD"
31
+ # Money.us_dollar(85).format(with_currency: true) #=> "$0.85 USD"
32
+ #
33
+ # @option rules [Boolean] :rounded_infinite_precision (false) Whether the
34
+ # amount of money should be rounded when using {infinite_precision}
35
+ #
36
+ # @example
37
+ # Money.us_dollar(100.1).format #=> "$1.001"
38
+ # Money.us_dollar(100.1).format(rounded_infinite_precision: true) #=> "$1"
39
+ # Money.us_dollar(100.9).format(rounded_infinite_precision: true) #=> "$1.01"
40
+ #
41
+ # @option rules [Boolean] :no_cents (false) Whether cents should be omitted.
42
+ #
43
+ # @example
44
+ # Money.ca_dollar(100).format(no_cents: true) #=> "$1"
45
+ # Money.ca_dollar(599).format(no_cents: true) #=> "$5"
46
+ #
47
+ # @option rules [Boolean] :no_cents_if_whole (false) Whether cents should be
48
+ # omitted if the cent value is zero
49
+ #
50
+ # @example
51
+ # Money.ca_dollar(10000).format(no_cents_if_whole: true) #=> "$100"
52
+ # Money.ca_dollar(10034).format(no_cents_if_whole: true) #=> "$100.34"
53
+ #
54
+ # @option rules [Boolean, String, nil] :symbol (true) Whether a money symbol
55
+ # should be prepended to the result string. The default is true. This method
56
+ # attempts to pick a symbol that's suitable for the given currency.
57
+ #
58
+ # @example
59
+ # Money.new(100, "USD") #=> "$1.00"
60
+ # Money.new(100, "GBP") #=> "£1.00"
61
+ # Money.new(100, "EUR") #=> "€1.00"
62
+ #
63
+ # # Same thing.
64
+ # Money.new(100, "USD").format(symbol: true) #=> "$1.00"
65
+ # Money.new(100, "GBP").format(symbol: true) #=> "£1.00"
66
+ # Money.new(100, "EUR").format(symbol: true) #=> "€1.00"
67
+ #
68
+ # # You can specify a false expression or an empty string to disable
69
+ # # prepending a money symbol.§
70
+ # Money.new(100, "USD").format(symbol: false) #=> "1.00"
71
+ # Money.new(100, "GBP").format(symbol: nil) #=> "1.00"
72
+ # Money.new(100, "EUR").format(symbol: "") #=> "1.00"
73
+ #
74
+ # # If the symbol for the given currency isn't known, then it will default
75
+ # # to "¤" as symbol.
76
+ # Money.new(100, "AWG").format(symbol: true) #=> "¤1.00"
77
+ #
78
+ # # You can specify a string as value to enforce using a particular symbol.
79
+ # Money.new(100, "AWG").format(symbol: "ƒ") #=> "ƒ1.00"
80
+ #
81
+ # # You can specify a indian currency format
82
+ # Money.new(10000000, "INR").format(south_asian_number_formatting: true) #=> "1,00,000.00"
83
+ # Money.new(10000000).format(south_asian_number_formatting: true) #=> "$1,00,000.00"
84
+ #
85
+ # @option rules [Boolean, nil] :symbol_before_without_space (true) Whether
86
+ # a space between the money symbol and the amount should be inserted when
87
+ # +:symbol_position+ is +:before+. The default is true (meaning no space). Ignored
88
+ # if +:symbol+ is false or +:symbol_position+ is not +:before+.
89
+ #
90
+ # @example
91
+ # # Default is to not insert a space.
92
+ # Money.new(100, "USD").format #=> "$1.00"
93
+ #
94
+ # # Same thing.
95
+ # Money.new(100, "USD").format(symbol_before_without_space: true) #=> "$1.00"
96
+ #
97
+ # # If set to false, will insert a space.
98
+ # Money.new(100, "USD").format(symbol_before_without_space: false) #=> "$ 1.00"
99
+ #
100
+ # @option rules [Boolean, nil] :symbol_after_without_space (false) Whether
101
+ # a space between the amount and the money symbol should be inserted when
102
+ # +:symbol_position+ is +:after+. The default is false (meaning space). Ignored
103
+ # if +:symbol+ is false or +:symbol_position+ is not +:after+.
104
+ #
105
+ # @example
106
+ # # Default is to insert a space.
107
+ # Money.new(100, "USD").format(symbol_position: :after) #=> "1.00 $"
108
+ #
109
+ # # If set to true, will not insert a space.
110
+ # Money.new(100, "USD").format(symbol_position: :after, symbol_after_without_space: true) #=> "1.00$"
111
+ #
112
+ # @option rules [Boolean, String, nil] :decimal_mark (true) Whether the
113
+ # currency should be separated by the specified character or '.'
114
+ #
115
+ # @example
116
+ # # If a string is specified, it's value is used.
117
+ # Money.new(100, "USD").format(decimal_mark: ",") #=> "$1,00"
118
+ #
119
+ # # If the decimal_mark for a given currency isn't known, then it will default
120
+ # # to "." as decimal_mark.
121
+ # Money.new(100, "FOO").format #=> "$1.00"
122
+ #
123
+ # @option rules [Boolean, String, nil] :thousands_separator (true) Whether
124
+ # the currency should be delimited by the specified character or ','
125
+ #
126
+ # @example
127
+ # # If false is specified, no thousands_separator is used.
128
+ # Money.new(100000, "USD").format(thousands_separator: false) #=> "1000.00"
129
+ # Money.new(100000, "USD").format(thousands_separator: nil) #=> "1000.00"
130
+ # Money.new(100000, "USD").format(thousands_separator: "") #=> "1000.00"
131
+ #
132
+ # # If a string is specified, it's value is used.
133
+ # Money.new(100000, "USD").format(thousands_separator: ".") #=> "$1.000.00"
134
+ #
135
+ # # If the thousands_separator for a given currency isn't known, then it will
136
+ # # default to "," as thousands_separator.
137
+ # Money.new(100000, "FOO").format #=> "$1,000.00"
138
+ #
139
+ # @option rules [Boolean] :html (false) Whether the currency should be
140
+ # HTML-formatted. Only useful in combination with +:with_currency+.
141
+ #
142
+ # @example
143
+ # Money.ca_dollar(570).format(html: true, with_currency: true)
144
+ # #=> "$5.70 <span class=\"currency\">CAD</span>"
145
+ #
146
+ # @option rules [Boolean] :html_wrap (false) Whether all currency parts should be HTML-formatted.
147
+ #
148
+ # @example
149
+ # Money.ca_dollar(570).format(html_wrap: true, with_currency: true)
150
+ # #=> "<span class=\"money-currency-symbol\">$</span><span class=\"money-whole\">5</span><span class=\"money-decimal-mark\">.</span><span class=\"money-decimal\">70</span> <span class=\"money-currency\">CAD</span>"
151
+ #
152
+ # @option rules [Boolean] :sign_before_symbol (false) Whether the sign should be
153
+ # before the currency symbol.
154
+ #
155
+ # @example
156
+ # # You can specify to display the sign before the symbol for negative numbers
157
+ # Money.new(-100, "GBP").format(sign_before_symbol: true) #=> "-£1.00"
158
+ # Money.new(-100, "GBP").format(sign_before_symbol: false) #=> "£-1.00"
159
+ # Money.new(-100, "GBP").format #=> "£-1.00"
160
+ #
161
+ # @option rules [Boolean] :sign_positive (false) Whether positive numbers should be
162
+ # signed, too.
163
+ #
164
+ # @example
165
+ # # You can specify to display the sign with positive numbers
166
+ # Money.new(100, "GBP").format(sign_positive: true, sign_before_symbol: true) #=> "+£1.00"
167
+ # Money.new(100, "GBP").format(sign_positive: true, sign_before_symbol: false) #=> "£+1.00"
168
+ # Money.new(100, "GBP").format(sign_positive: false, sign_before_symbol: true) #=> "£1.00"
169
+ # Money.new(100, "GBP").format(sign_positive: false, sign_before_symbol: false) #=> "£1.00"
170
+ # Money.new(100, "GBP").format #=> "£+1.00"
171
+ #
172
+ # @option rules [Boolean] :disambiguate (false) Prevents the result from being ambiguous
173
+ # due to equal symbols for different currencies. Uses the `disambiguate_symbol`.
174
+ #
175
+ # @example
176
+ # Money.new(10000, "USD").format(disambiguate: false) #=> "$100.00"
177
+ # Money.new(10000, "CAD").format(disambiguate: false) #=> "$100.00"
178
+ # Money.new(10000, "USD").format(disambiguate: true) #=> "$100.00"
179
+ # Money.new(10000, "CAD").format(disambiguate: true) #=> "C$100.00"
180
+ #
181
+ # @option rules [Boolean] :html_wrap_symbol (false) Wraps the currency symbol
182
+ # in a html <span> tag.
183
+ #
184
+ # @example
185
+ # Money.new(10000, "USD").format(disambiguate: false)
186
+ # #=> "<span class=\"currency_symbol\">$100.00</span>
187
+ #
188
+ # @option rules [Symbol] :symbol_position (:before) `:before` if the currency
189
+ # symbol goes before the amount, `:after` if it goes after.
190
+ #
191
+ # @example
192
+ # Money.new(10000, "USD").format(symbol_position: :before) #=> "$100.00"
193
+ # Money.new(10000, "USD").format(symbol_position: :after) #=> "100.00 $"
194
+ #
195
+ # @option rules [Boolean] :translate (true) `true` Checks for custom
196
+ # symbol definitions using I18n.
197
+ #
198
+ # @example
199
+ # # With the following entry in the translation files:
200
+ # # en:
201
+ # # number:
202
+ # # currency:
203
+ # # symbol:
204
+ # # CAD: "CAD$"
205
+ # Money.new(10000, "CAD").format(translate: true) #=> "CAD$100.00"
206
+ #
207
+ # @example
208
+ # Money.new(89000, :btc).format(drop_trailing_zeros: true) #=> B⃦0.00089
209
+ # Money.new(110, :usd).format(drop_trailing_zeros: true) #=> $1.1
210
+ #
211
+ # @option rules [String] :format (nil) Provide a template for formatting. `%u` will be replaced
212
+ # with the symbol (if present) and `%n` will be replaced with the number.
213
+ #
214
+ # @example
215
+ # Money.new(10000, "USD").format(format: '%u %n') #=> "$ 100.00"
216
+ # Money.new(10000, "USD").format(format: '<span>%u%n</span>') #=> "<span>$100.00</span>"
217
+ #
218
+ # Note that the default rules can be defined through {Money.default_formatting_rules} hash.
219
+ #
220
+ # @see Money.default_formatting_rules Money.default_formatting_rules for more information.
221
+ def initialize(money, *rules)
222
+ @money = money
223
+ @currency = money.currency
224
+ @rules = FormattingRules.new(@currency, *rules)
225
+ end
226
+
227
+ def to_s
228
+ return free_text if show_free_text?
229
+ result = format_number
230
+ formatted = append_sign(result)
231
+ append_currency_symbol(formatted)
232
+ end
233
+
234
+ def thousands_separator
235
+ lookup :thousands_separator
236
+ end
237
+
238
+ def decimal_mark
239
+ lookup :decimal_mark
240
+ end
241
+
242
+ alias_method :delimiter, :thousands_separator
243
+ alias_method :separator, :decimal_mark
244
+
245
+ private
246
+
247
+ attr_reader :money, :currency, :rules
248
+
249
+ def format_number
250
+ whole_part, decimal_part = extract_whole_and_decimal_parts
251
+
252
+ # Format whole and decimal parts separately
253
+ decimal_part = format_decimal_part(decimal_part)
254
+ whole_part = format_whole_part(whole_part)
255
+
256
+ # Assemble the final formatted amount
257
+ if rules[:html_wrap]
258
+ if decimal_part.nil?
259
+ html_wrap(whole_part, "whole")
260
+ else
261
+ [
262
+ html_wrap(whole_part, "whole"),
263
+ html_wrap(decimal_mark, "decimal-mark"),
264
+ html_wrap(decimal_part, "decimal")
265
+ ].join
266
+ end
267
+ else
268
+ [whole_part, decimal_part].compact.join(decimal_mark)
269
+ end
270
+ end
271
+
272
+ def append_sign(formatted_number)
273
+ sign = money.negative? ? '-' : ''
274
+
275
+ if rules[:sign_positive] == true && money.positive?
276
+ sign = '+'
277
+ end
278
+
279
+ if rules[:sign_before_symbol] == true
280
+ sign_before = sign
281
+ sign = ''
282
+ end
283
+
284
+ symbol_value = symbol_value_from(rules)
285
+
286
+ if symbol_value && !symbol_value.empty?
287
+ if rules[:html_wrap_symbol]
288
+ symbol_value = "<span class=\"currency_symbol\">#{symbol_value}</span>"
289
+ elsif rules[:html_wrap]
290
+ symbol_value = html_wrap(symbol_value, "currency-symbol")
291
+ end
292
+
293
+ rules[:format]
294
+ .gsub('%u', [sign_before, symbol_value].join)
295
+ .gsub('%n', [sign, formatted_number].join)
296
+ else
297
+ formatted_number = "#{sign_before}#{sign}#{formatted_number}"
298
+ end
299
+ end
300
+
301
+ def append_currency_symbol(formatted_number)
302
+ if rules[:with_currency]
303
+ formatted_number << " "
304
+
305
+ if rules[:html]
306
+ formatted_number << "<span class=\"currency\">#{currency.to_s}</span>"
307
+ elsif rules[:html_wrap]
308
+ formatted_number << html_wrap(currency.to_s, "currency")
309
+ else
310
+ formatted_number << currency.to_s
311
+ end
312
+ end
313
+ formatted_number
314
+ end
315
+
316
+ def show_free_text?
317
+ money.zero? && rules[:display_free]
318
+ end
319
+
320
+ def html_wrap(string, class_name)
321
+ "<span class=\"money-#{class_name}\">#{string}</span>"
322
+ end
323
+
324
+ def free_text
325
+ rules[:display_free].respond_to?(:to_str) ? rules[:display_free] : 'free'
326
+ end
327
+
328
+ def format_whole_part(value)
329
+ # Apply thousands_separator
330
+ value.gsub regexp_format, "\\1#{thousands_separator}"
331
+ end
332
+
333
+ def extract_whole_and_decimal_parts
334
+ fractional = money.fractional.abs
335
+
336
+ # Round the infinite precision part if needed
337
+ fractional = fractional.round if rules[:rounded_infinite_precision]
338
+
339
+ # Translate subunits into units
340
+ fractional_units = BigDecimal(fractional) / currency.subunit_to_unit
341
+
342
+ # Split the result and return whole and decimal parts separately
343
+ fractional_units.to_s('F').split('.')
344
+ end
345
+
346
+ def format_decimal_part(value)
347
+ return nil if currency.decimal_places == 0 && !Money.infinite_precision
348
+ return nil if rules[:no_cents]
349
+ return nil if rules[:no_cents_if_whole] && value.to_i == 0
350
+
351
+ # Pad value, making up for missing zeroes at the end
352
+ value = value.ljust(currency.decimal_places, '0')
353
+
354
+ # Drop trailing zeros if needed
355
+ value.gsub!(/0*$/, '') if rules[:drop_trailing_zeros]
356
+
357
+ value.empty? ? nil : value
358
+ end
359
+
360
+ def lookup(key)
361
+ return rules[key] || DEFAULTS[key] if rules.has_key?(key)
362
+
363
+ (Money.locale_backend && Money.locale_backend.lookup(key, currency)) || DEFAULTS[key]
364
+ end
365
+
366
+ def regexp_format
367
+ if rules[:south_asian_number_formatting]
368
+ # from http://blog.revathskumar.com/2014/11/regex-comma-seperated-indian-currency-format.html
369
+ /(\d+?)(?=(\d\d)+(\d)(?!\d))(\.\d+)?/
370
+ else
371
+ /(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/
372
+ end
373
+ end
374
+
375
+ def symbol_value_from(rules)
376
+ if rules.has_key?(:symbol)
377
+ if rules[:symbol] === true
378
+ if rules[:disambiguate] && currency.disambiguate_symbol
379
+ currency.disambiguate_symbol
380
+ else
381
+ money.symbol
382
+ end
383
+ elsif rules[:symbol]
384
+ rules[:symbol]
385
+ else
386
+ ""
387
+ end
388
+ elsif rules[:html] || rules[:html_wrap]
389
+ currency.html_entity == '' ? currency.symbol : currency.html_entity
390
+ elsif rules[:disambiguate] && currency.disambiguate_symbol
391
+ currency.disambiguate_symbol
392
+ else
393
+ money.symbol
394
+ end
395
+ end
396
+ end
397
+ end