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.
- checksums.yaml +4 -4
- data/.rspec +2 -1
- data/.travis.yml +22 -5
- data/AUTHORS +5 -0
- data/CHANGELOG.md +109 -3
- data/Gemfile +13 -4
- data/LICENSE +2 -0
- data/README.md +69 -49
- data/config/currency_backwards_compatible.json +30 -0
- data/config/currency_iso.json +139 -62
- data/config/currency_non_iso.json +66 -2
- data/lib/money.rb +0 -13
- data/lib/money/bank/variable_exchange.rb +9 -22
- data/lib/money/currency.rb +35 -38
- data/lib/money/currency/heuristics.rb +1 -144
- data/lib/money/currency/loader.rb +1 -1
- data/lib/money/locale_backend/base.rb +7 -0
- data/lib/money/locale_backend/errors.rb +6 -0
- data/lib/money/locale_backend/i18n.rb +24 -0
- data/lib/money/locale_backend/legacy.rb +28 -0
- data/lib/money/money.rb +120 -151
- data/lib/money/money/allocation.rb +37 -0
- data/lib/money/money/arithmetic.rb +57 -52
- data/lib/money/money/constructors.rb +1 -2
- data/lib/money/money/formatter.rb +397 -0
- data/lib/money/money/formatting_rules.rb +120 -0
- data/lib/money/money/locale_backend.rb +20 -0
- data/lib/money/rates_store/memory.rb +1 -2
- data/lib/money/version.rb +1 -1
- data/money.gemspec +10 -16
- data/spec/bank/variable_exchange_spec.rb +7 -3
- data/spec/currency/heuristics_spec.rb +2 -153
- data/spec/currency_spec.rb +45 -4
- data/spec/locale_backend/i18n_spec.rb +62 -0
- data/spec/locale_backend/legacy_spec.rb +74 -0
- data/spec/money/allocation_spec.rb +130 -0
- data/spec/money/arithmetic_spec.rb +217 -104
- data/spec/money/constructors_spec.rb +0 -12
- data/spec/money/formatting_spec.rb +320 -179
- data/spec/money/locale_backend_spec.rb +14 -0
- data/spec/money_spec.rb +159 -26
- data/spec/rates_store/memory_spec.rb +13 -2
- data/spec/spec_helper.rb +2 -0
- data/spec/support/shared_examples/money_examples.rb +14 -0
- metadata +32 -41
- 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 [
|
51
|
+
# @return [Integer]
|
57
52
|
#
|
58
53
|
# @raise [TypeError] when other object is not Money
|
59
54
|
#
|
60
|
-
def <=>(
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
60
|
+
return 0 if zero? && other.zero?
|
61
|
+
other = other.exchange_to(currency)
|
62
|
+
fractional <=> other.fractional
|
66
63
|
rescue Money::Bank::UnknownRate
|
67
|
-
|
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
|
-
|
107
|
-
|
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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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 [
|
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
|
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
|
-
|
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,
|
201
|
+
# @param [Money, Integer] val Number to divmod by.
|
193
202
|
#
|
194
|
-
# @return [Array<Money,Money>,Array<
|
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
|
-
|
216
|
-
|
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,
|
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,
|
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,
|
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
|
-
[
|
312
|
+
[self, CoercedNumeric.new(other)]
|
308
313
|
end
|
309
314
|
end
|
310
315
|
end
|
@@ -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
|