papercavalier-money 3.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,244 @@
1
+ class Money
2
+ module Formatting
3
+
4
+ if Object.const_defined?("I18n")
5
+ def thousands_separator
6
+ if self.class.use_i18n
7
+ I18n.t(
8
+ :"number.currency.format.delimiter",
9
+ :default => I18n.t(
10
+ :"number.format.delimiter",
11
+ :default => (currency.thousands_separator || ",")
12
+ )
13
+ )
14
+ else
15
+ currency.thousands_separator || ","
16
+ end
17
+ end
18
+ else
19
+ def thousands_separator
20
+ currency.thousands_separator || ","
21
+ end
22
+ end
23
+ alias :delimiter :thousands_separator
24
+
25
+
26
+ if Object.const_defined?("I18n")
27
+ def decimal_mark
28
+ if self.class.use_i18n
29
+ I18n.t(
30
+ :"number.currency.format.separator",
31
+ :default => I18n.t(
32
+ :"number.format.separator",
33
+ :default => (currency.decimal_mark || ".")
34
+ )
35
+ )
36
+ else
37
+ currency.decimal_mark || "."
38
+ end
39
+ end
40
+ else
41
+ def decimal_mark
42
+ currency.decimal_mark || "."
43
+ end
44
+ end
45
+ alias :separator :decimal_mark
46
+
47
+ # Creates a formatted price string according to several rules.
48
+ #
49
+ # @param [Hash] *rules The options used to format the string.
50
+ #
51
+ # @return [String]
52
+ #
53
+ # @option *rules [Boolean, String] :display_free (false) Whether a zero
54
+ # amount of money should be formatted of "free" or as the supplied string.
55
+ #
56
+ # @example
57
+ # Money.us_dollar(0).format(:display_free => true) #=> "free"
58
+ # Money.us_dollar(0).format(:display_free => "gratis") #=> "gratis"
59
+ # Money.us_dollar(0).format #=> "$0.00"
60
+ #
61
+ # @option *rules [Boolean] :with_currency (false) Whether the currency name
62
+ # should be appended to the result string.
63
+ #
64
+ # @example
65
+ # Money.ca_dollar(100).format => "$1.00"
66
+ # Money.ca_dollar(100).format(:with_currency => true) #=> "$1.00 CAD"
67
+ # Money.us_dollar(85).format(:with_currency => true) #=> "$0.85 USD"
68
+ #
69
+ # @option *rules [Boolean] :no_cents (false) Whether cents should be omitted.
70
+ #
71
+ # @example
72
+ # Money.ca_dollar(100).format(:no_cents => true) #=> "$1"
73
+ # Money.ca_dollar(599).format(:no_cents => true) #=> "$5"
74
+ #
75
+ # @option *rules [Boolean] :no_cents_if_whole (false) Whether cents should be
76
+ # omitted if the cent value is zero
77
+ #
78
+ # @example
79
+ # Money.ca_dollar(10000).format(:no_cents_if_whole => true) #=> "$100"
80
+ # Money.ca_dollar(10034).format(:no_cents_if_whole => true) #=> "$100.34"
81
+ #
82
+ # @option *rules [Boolean, String, nil] :symbol (true) Whether a money symbol
83
+ # should be prepended to the result string. The default is true. This method
84
+ # attempts to pick a symbol that's suitable for the given currency.
85
+ #
86
+ # @example
87
+ # Money.new(100, "USD") #=> "$1.00"
88
+ # Money.new(100, "GBP") #=> "£1.00"
89
+ # Money.new(100, "EUR") #=> "€1.00"
90
+ #
91
+ # # Same thing.
92
+ # Money.new(100, "USD").format(:symbol => true) #=> "$1.00"
93
+ # Money.new(100, "GBP").format(:symbol => true) #=> "£1.00"
94
+ # Money.new(100, "EUR").format(:symbol => true) #=> "€1.00"
95
+ #
96
+ # # You can specify a false expression or an empty string to disable
97
+ # # prepending a money symbol.§
98
+ # Money.new(100, "USD").format(:symbol => false) #=> "1.00"
99
+ # Money.new(100, "GBP").format(:symbol => nil) #=> "1.00"
100
+ # Money.new(100, "EUR").format(:symbol => "") #=> "1.00"
101
+ #
102
+ # # If the symbol for the given currency isn't known, then it will default
103
+ # # to "¤" as symbol.
104
+ # Money.new(100, "AWG").format(:symbol => true) #=> "¤1.00"
105
+ #
106
+ # # You can specify a string as value to enforce using a particular symbol.
107
+ # Money.new(100, "AWG").format(:symbol => "ƒ") #=> "ƒ1.00"
108
+ #
109
+ # @option *rules [Boolean, String, nil] :decimal_mark (true) Whether the
110
+ # currency should be separated by the specified character or '.'
111
+ #
112
+ # @example
113
+ # # If a string is specified, it's value is used.
114
+ # Money.new(100, "USD").format(:decimal_mark => ",") #=> "$1,00"
115
+ #
116
+ # # If the decimal_mark for a given currency isn't known, then it will default
117
+ # # to "." as decimal_mark.
118
+ # Money.new(100, "FOO").format #=> "$1.00"
119
+ #
120
+ # @option *rules [Boolean, String, nil] :thousands_separator (true) Whether
121
+ # the currency should be delimited by the specified character or ','
122
+ #
123
+ # @example
124
+ # # If false is specified, no thousands_separator is used.
125
+ # Money.new(100000, "USD").format(:thousands_separator => false) #=> "1000.00"
126
+ # Money.new(100000, "USD").format(:thousands_separator => nil) #=> "1000.00"
127
+ # Money.new(100000, "USD").format(:thousands_separator => "") #=> "1000.00"
128
+ #
129
+ # # If a string is specified, it's value is used.
130
+ # Money.new(100000, "USD").format(:thousands_separator => ".") #=> "$1.000.00"
131
+ #
132
+ # # If the thousands_separator for a given currency isn't known, then it will
133
+ # # default to "," as thousands_separator.
134
+ # Money.new(100000, "FOO").format #=> "$1,000.00"
135
+ #
136
+ # @option *rules [Boolean] :html (false) Whether the currency should be
137
+ # HTML-formatted. Only useful in combination with +:with_currency+.
138
+ #
139
+ # @example
140
+ # s = Money.ca_dollar(570).format(:html => true, :with_currency => true)
141
+ # s #=> "$5.70 <span class=\"currency\">CAD</span>"
142
+ def format(*rules)
143
+ # support for old format parameters
144
+ rules = normalize_formatting_rules(rules)
145
+
146
+ if cents == 0
147
+ if rules[:display_free].respond_to?(:to_str)
148
+ return rules[:display_free]
149
+ elsif rules[:display_free]
150
+ return "free"
151
+ end
152
+ end
153
+
154
+ symbol_value =
155
+ if rules.has_key?(:symbol)
156
+ if rules[:symbol] === true
157
+ symbol
158
+ elsif rules[:symbol]
159
+ rules[:symbol]
160
+ else
161
+ ""
162
+ end
163
+ elsif rules[:html]
164
+ currency.html_entity
165
+ else
166
+ symbol
167
+ end
168
+
169
+ formatted = case rules[:no_cents]
170
+ when true
171
+ "#{self.to_s.to_i}"
172
+ else
173
+ "#{self.to_s}"
174
+ end
175
+
176
+ if rules[:no_cents_if_whole] && cents % currency.subunit_to_unit == 0
177
+ formatted = "#{self.to_s.to_i}"
178
+ end
179
+
180
+ symbol_position =
181
+ if rules.has_key?(:symbol_position)
182
+ rules[:symbol_position]
183
+ elsif currency.symbol_first?
184
+ :before
185
+ else
186
+ :after
187
+ end
188
+
189
+ if symbol_value && !symbol_value.empty?
190
+ formatted = (symbol_position == :before ? "#{symbol_value}#{formatted}" : "#{formatted} #{symbol_value}")
191
+ end
192
+
193
+ if rules.has_key?(:decimal_mark) and rules[:decimal_mark] and
194
+ rules[:decimal_mark] != decimal_mark
195
+ formatted.sub!(decimal_mark, rules[:decimal_mark])
196
+ end
197
+
198
+ thousands_separator_value = thousands_separator
199
+ # Determine thousands_separator
200
+ if rules.has_key?(:thousands_separator)
201
+ if rules[:thousands_separator] === false or rules[:thousands_separator].nil?
202
+ thousands_separator_value = ""
203
+ elsif rules[:thousands_separator]
204
+ thousands_separator_value = rules[:thousands_separator]
205
+ end
206
+ end
207
+
208
+ # Apply thousands_separator
209
+ formatted.gsub!(/(\d)(?=(?:\d{3})+(?:[^\d]|$))/, "\\1#{thousands_separator_value}")
210
+
211
+ if rules[:with_currency]
212
+ formatted << " "
213
+ formatted << '<span class="currency">' if rules[:html]
214
+ formatted << currency.to_s
215
+ formatted << '</span>' if rules[:html]
216
+ end
217
+ formatted
218
+ end
219
+
220
+
221
+ private
222
+
223
+ # Cleans up formatting rules.
224
+ #
225
+ # @param [Hash]
226
+ #
227
+ # @return [Hash]
228
+ def normalize_formatting_rules(rules)
229
+ if rules.size == 0
230
+ rules = {}
231
+ elsif rules.size == 1
232
+ rules = rules.pop
233
+ rules = { rules => true } if rules.is_a?(Symbol)
234
+ end
235
+ if not rules.include?(:decimal_mark) and rules.include?(:separator)
236
+ rules[:decimal_mark] = rules[:separator]
237
+ end
238
+ if not rules.include?(:thousands_separator) and rules.include?(:delimiter)
239
+ rules[:thousands_separator] = rules[:delimiter]
240
+ end
241
+ rules
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,350 @@
1
+ class Money
2
+ module Parsing
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ # Parses the current string and converts it to a +Money+ object.
9
+ # Excess characters will be discarded.
10
+ #
11
+ # @param [String, #to_s] input The input to parse.
12
+ # @param [Currency, String, Symbol] currency The currency format.
13
+ # The currency to set the resulting +Money+ object to.
14
+ #
15
+ # @return [Money]
16
+ #
17
+ # @raise [ArgumentError] If any +currency+ is supplied and
18
+ # given value doesn't match the one extracted from
19
+ # the +input+ string.
20
+ #
21
+ # @example
22
+ # '100'.to_money #=> #<Money @cents=10000>
23
+ # '100.37'.to_money #=> #<Money @cents=10037>
24
+ # '100 USD'.to_money #=> #<Money @cents=10000, @currency=#<Money::Currency id: usd>>
25
+ # 'USD 100'.to_money #=> #<Money @cents=10000, @currency=#<Money::Currency id: usd>>
26
+ # '$100 USD'.to_money #=> #<Money @cents=10000, @currency=#<Money::Currency id: usd>>
27
+ # 'hello 2000 world'.to_money #=> #<Money @cents=200000 @currency=#<Money::Currency id: usd>>
28
+ #
29
+ # @example Mismatching currencies
30
+ # 'USD 2000'.to_money("EUR") #=> ArgumentError
31
+ #
32
+ # @see Money.from_string
33
+ #
34
+ def parse(input, currency = nil)
35
+ i = input.to_s
36
+
37
+ # Get the currency.
38
+ m = i.scan /([A-Z]{2,3})/
39
+ c = m[0] ? m[0][0] : nil
40
+
41
+ # check that currency passed and embedded currency are the same,
42
+ # and negotiate the final currency
43
+ if currency.nil? and c.nil?
44
+ currency = Money.default_currency
45
+ elsif currency.nil?
46
+ currency = c
47
+ elsif c.nil?
48
+ currency = currency
49
+ elsif currency != c
50
+ # TODO: ParseError
51
+ raise ArgumentError, "Mismatching Currencies"
52
+ end
53
+ currency = Money::Currency.wrap(currency)
54
+
55
+ cents = extract_cents(i, currency)
56
+ new(cents, currency)
57
+ end
58
+
59
+ # Converts a String into a Money object treating the +value+
60
+ # as dollars and converting them to the corresponding cents value,
61
+ # according to +currency+ subunit property,
62
+ # before instantiating the Money object.
63
+ #
64
+ # Behind the scenes, this method relies on {Money.from_bigdecimal}
65
+ # to avoid problems with string-to-numeric conversion.
66
+ #
67
+ # @param [String, #to_s] value The money amount, in dollars.
68
+ # @param [Currency, String, Symbol] currency
69
+ # The currency to set the resulting +Money+ object to.
70
+ #
71
+ # @return [Money]
72
+ #
73
+ # @example
74
+ # Money.from_string("100")
75
+ # #=> #<Money @cents=10000 @currency="USD">
76
+ # Money.from_string("100", "USD")
77
+ # #=> #<Money @cents=10000 @currency="USD">
78
+ # Money.from_string("100", "EUR")
79
+ # #=> #<Money @cents=10000 @currency="EUR">
80
+ # Money.from_string("100", "BHD")
81
+ # #=> #<Money @cents=100 @currency="BHD">
82
+ #
83
+ # @see String#to_money
84
+ # @see Money.parse
85
+ #
86
+ def from_string(value, currency = Money.default_currency)
87
+ from_bigdecimal(BigDecimal.new(value.to_s), currency)
88
+ end
89
+
90
+ # Converts a Fixnum into a Money object treating the +value+
91
+ # as dollars and converting them to the corresponding cents value,
92
+ # according to +currency+ subunit property,
93
+ # before instantiating the Money object.
94
+ #
95
+ # @param [Fixnum] value The money amount, in dollars.
96
+ # @param [Currency, String, Symbol] currency The currency format.
97
+ #
98
+ # @return [Money]
99
+ #
100
+ # @example
101
+ # Money.from_fixnum(100)
102
+ # #=> #<Money @cents=10000 @currency="USD">
103
+ # Money.from_fixnum(100, "USD")
104
+ # #=> #<Money @cents=10000 @currency="USD">
105
+ # Money.from_fixnum(100, "EUR")
106
+ # #=> #<Money @cents=10000 @currency="EUR">
107
+ # Money.from_fixnum(100, "BHD")
108
+ # #=> #<Money @cents=100 @currency="BHD">
109
+ #
110
+ # @see Fixnum#to_money
111
+ # @see Money.from_numeric
112
+ #
113
+ def from_fixnum(value, currency = Money.default_currency)
114
+ currency = Money::Currency.wrap(currency)
115
+ amount = value * currency.subunit_to_unit
116
+ new(amount, currency)
117
+ end
118
+
119
+ # Converts a Float into a Money object treating the +value+
120
+ # as dollars and converting them to the corresponding cents value,
121
+ # according to +currency+ subunit property,
122
+ # before instantiating the Money object.
123
+ #
124
+ # Behind the scenes, this method relies on Money.from_bigdecimal
125
+ # to avoid problems with floating point precision.
126
+ #
127
+ # @param [Float] value The money amount, in dollars.
128
+ # @param [Currency, String, Symbol] currency The currency format.
129
+ #
130
+ # @return [Money]
131
+ #
132
+ # @example
133
+ # Money.from_float(100.0)
134
+ # #=> #<Money @cents=10000 @currency="USD">
135
+ # Money.from_float(100.0, "USD")
136
+ # #=> #<Money @cents=10000 @currency="USD">
137
+ # Money.from_float(100.0, "EUR")
138
+ # #=> #<Money @cents=10000 @currency="EUR">
139
+ # Money.from_float(100.0, "BHD")
140
+ # #=> #<Money @cents=100 @currency="BHD">
141
+ #
142
+ # @see Float#to_money
143
+ # @see Money.from_numeric
144
+ #
145
+ def from_float(value, currency = Money.default_currency)
146
+ from_bigdecimal(BigDecimal.new(value.to_s), currency)
147
+ end
148
+
149
+ # Converts a BigDecimal into a Money object treating the +value+
150
+ # as dollars and converting them to the corresponding cents value,
151
+ # according to +currency+ subunit property,
152
+ # before instantiating the Money object.
153
+ #
154
+ # @param [BigDecimal] value The money amount, in dollars.
155
+ # @param [Currency, String, Symbol] currency The currency format.
156
+ #
157
+ # @return [Money]
158
+ #
159
+ # @example
160
+ # Money.from_bigdecimal(BigDecimal.new("100")
161
+ # #=> #<Money @cents=10000 @currency="USD">
162
+ # Money.from_bigdecimal(BigDecimal.new("100", "USD")
163
+ # #=> #<Money @cents=10000 @currency="USD">
164
+ # Money.from_bigdecimal(BigDecimal.new("100", "EUR")
165
+ # #=> #<Money @cents=10000 @currency="EUR">
166
+ # Money.from_bigdecimal(BigDecimal.new("100", "BHD")
167
+ # #=> #<Money @cents=100 @currency="BHD">
168
+ #
169
+ # @see BigDecimal#to_money
170
+ # @see Money.from_numeric
171
+ #
172
+ def from_bigdecimal(value, currency = Money.default_currency)
173
+ currency = Money::Currency.wrap(currency)
174
+ amount = value * currency.subunit_to_unit
175
+ new(amount.fix, currency)
176
+ end
177
+
178
+ # Converts a Numeric value into a Money object treating the +value+
179
+ # as dollars and converting them to the corresponding cents value,
180
+ # according to +currency+ subunit property,
181
+ # before instantiating the Money object.
182
+ #
183
+ # This method relies on various +Money.from_*+ methods
184
+ # and tries to forwards the call to the most appropriate method
185
+ # in order to reduce computation effort.
186
+ # For instance, if +value+ is an Integer, this method calls
187
+ # {Money.from_fixnum} instead of using the default
188
+ # {Money.from_bigdecimal} which adds the overload to converts
189
+ # the value into a slower BigDecimal instance.
190
+ #
191
+ # @param [Numeric] value The money amount, in dollars.
192
+ # @param [Currency, String, Symbol] currency The currency format.
193
+ #
194
+ # @return [Money]
195
+ #
196
+ # @raise +ArgumentError+ Unless +value+ is a supported type.
197
+ #
198
+ # @example
199
+ # Money.from_numeric(100)
200
+ # #=> #<Money @cents=10000 @currency="USD">
201
+ # Money.from_numeric(100.00)
202
+ # #=> #<Money @cents=10000 @currency="USD">
203
+ # Money.from_numeric("100")
204
+ # #=> ArgumentError
205
+ #
206
+ # @see Numeric#to_money
207
+ # @see Money.from_fixnum
208
+ # @see Money.from_float
209
+ # @see Money.from_bigdecimal
210
+ #
211
+ def from_numeric(value, currency = Money.default_currency)
212
+ case value
213
+ when Fixnum
214
+ from_fixnum(value, currency)
215
+ when Numeric
216
+ from_bigdecimal(BigDecimal.new(value.to_s), currency)
217
+ else
218
+ raise ArgumentError, "`value' should be a Numeric object"
219
+ end
220
+ end
221
+
222
+ # Takes a number string and attempts to massage out the number.
223
+ #
224
+ # @param [String] input The string containing a potential number.
225
+ #
226
+ # @return [Integer]
227
+ #
228
+ def extract_cents(input, currency = Money.default_currency)
229
+ # remove anything that's not a number, potential thousands_separator, or minus sign
230
+ num = input.gsub(/[^\d|\.|,|\'|\-]/, '').strip
231
+
232
+ # set a boolean flag for if the number is negative or not
233
+ negative = num.split(//).first == "-"
234
+
235
+ # if negative, remove the minus sign from the number
236
+ # if it's not negative, the hyphen makes the value invalid
237
+ if negative
238
+ num = num.gsub(/^-/, '')
239
+ else
240
+ raise ArgumentError, "Invalid currency amount (hyphen)" if num.include?('-')
241
+ end
242
+
243
+ #if the number ends with punctuation, just throw it out. If it means decimal,
244
+ #it won't hurt anything. If it means a literal period or comma, this will
245
+ #save it from being mis-interpreted as a decimal.
246
+ num.chop! if num.match /[\.|,]$/
247
+
248
+ # gather all decimal_marks within the result number
249
+ used_decimal_marks = num.scan /[^\d]/
250
+
251
+ # determine the number of unique decimal_marks within the number
252
+ #
253
+ # e.g.
254
+ # $1,234,567.89 would return 2 (, and .)
255
+ # $125,00 would return 1
256
+ # $199 would return 0
257
+ # $1 234,567.89 would raise an error (decimal_marks are space, comma, and period)
258
+ case used_decimal_marks.uniq.length
259
+ # no decimal_mark or thousands_separator; major (dollars) is the number, and minor (cents) is 0
260
+ when 0 then major, minor = num, 0
261
+
262
+ # two decimal_marks, so we know the last item in this array is the
263
+ # major/minor thousands_separator and the rest are decimal_marks
264
+ when 2
265
+ decimal_mark, thousands_separator = used_decimal_marks.uniq
266
+ # remove all decimal_marks, split on the thousands_separator
267
+ major, minor = num.gsub(decimal_mark, '').split(thousands_separator)
268
+ min = 0 unless min
269
+ when 1
270
+ # we can't determine if the comma or period is supposed to be a decimal_mark or a thousands_separator
271
+ # e.g.
272
+ # 1,00 - comma is a thousands_separator
273
+ # 1.000 - period is a thousands_separator
274
+ # 1,000 - comma is a decimal_mark
275
+ # 1,000,000 - comma is a decimal_mark
276
+ # 10000,00 - comma is a thousands_separator
277
+ # 1000,000 - comma is a thousands_separator
278
+
279
+ # assign first decimal_mark for reusability
280
+ decimal_mark = used_decimal_marks.first
281
+
282
+ # decimal_mark is used as a decimal_mark when there are multiple instances, always
283
+ if num.scan(decimal_mark).length > 1 # multiple matches; treat as decimal_mark
284
+ major, minor = num.gsub(decimal_mark, ''), 0
285
+ else
286
+ # ex: 1,000 - 1.0000 - 10001.000
287
+ # split number into possible major (dollars) and minor (cents) values
288
+ possible_major, possible_minor = num.split(decimal_mark)
289
+ possible_major ||= "0"
290
+ possible_minor ||= "00"
291
+
292
+ # if the minor (cents) length isn't 3, assign major/minor from the possibles
293
+ # e.g.
294
+ # 1,00 => 1.00
295
+ # 1.0000 => 1.00
296
+ # 1.2 => 1.20
297
+ if possible_minor.length != 3 # thousands_separator
298
+ major, minor = possible_major, possible_minor
299
+ else
300
+ # minor length is three
301
+ # let's try to figure out intent of the thousands_separator
302
+
303
+ # the major length is greater than three, which means
304
+ # the comma or period is used as a thousands_separator
305
+ # e.g.
306
+ # 1000,000
307
+ # 100000,000
308
+ if possible_major.length > 3
309
+ major, minor = possible_major, possible_minor
310
+ else
311
+ # number is in format ###{sep}### or ##{sep}### or #{sep}###
312
+ # handle as , is sep, . is thousands_separator
313
+ if decimal_mark == '.'
314
+ major, minor = possible_major, possible_minor
315
+ else
316
+ major, minor = "#{possible_major}#{possible_minor}", 0
317
+ end
318
+ end
319
+ end
320
+ end
321
+ else
322
+ # TODO: ParseError
323
+ raise ArgumentError, "Invalid currency amount"
324
+ end
325
+
326
+ # build the string based on major/minor since decimal_mark/thousands_separator have been removed
327
+ # avoiding floating point arithmetic here to ensure accuracy
328
+ cents = (major.to_i * currency.subunit_to_unit)
329
+ # Because of an bug in JRuby, we can't just call #floor
330
+ minor = minor.to_s
331
+ minor = if minor.size < currency.decimal_places
332
+ (minor + ("0" * currency.decimal_places))[0,currency.decimal_places].to_i
333
+ elsif minor.size > currency.decimal_places
334
+ if minor[currency.decimal_places,1].to_i >= 5
335
+ minor[0,currency.decimal_places].to_i+1
336
+ else
337
+ minor[0,currency.decimal_places].to_i
338
+ end
339
+ else
340
+ minor.to_i
341
+ end
342
+ cents += minor
343
+
344
+ # if negative, multiply by -1; otherwise, return positive cents
345
+ negative ? cents * -1 : cents
346
+ end
347
+
348
+ end
349
+ end
350
+ end