2
2
3
3
require 'active_support/core_ext/big_decimal/conversions'
4
4
require 'active_support/core_ext/object/blank'
5
+ require 'active_support/core_ext/numeric'
5
6
require 'active_support/core_ext/string/output_safety'
7
+ require 'active_support/number_helper'
8
+ require 'erb'
6
9
7
10
module ActionView
8
11
# = Action View Number Helpers
9
12
module Helpers #:nodoc:
10
13
14
+
11
15
# Provides methods for converting numbers into formatted strings.
12
16
# Methods are provided for phone numbers, currency, percentage,
13
17
# precision, positional notation, file size and pretty printing.
@@ -16,9 +20,6 @@ module Helpers #:nodoc:
16
20
# unchanged if can't be converted into a valid number.
17
21
module NumberHelper
18
22
19
- DEFAULT_CURRENCY_VALUES = { :format => "%u%n" , :negative_format => "-%u%n" , :unit => "$" , :separator => "." , :delimiter => "," ,
20
- :precision => 2 , :significant => false , :strip_insignificant_zeros => false }
21
-
22
23
# Raised when argument +number+ param given to the helpers is invalid and
23
24
# the option :raise is set to +true+.
24
25
class InvalidNumberError < StandardError
@@ -63,25 +64,7 @@ def number_to_phone(number, options = {})
63
64
options = options . symbolize_keys
64
65
65
66
parse_float ( number , true ) if options [ :raise ]
66
-
67
- number = number . to_s . strip
68
- area_code = options [ :area_code ]
69
- delimiter = options [ :delimiter ] || "-"
70
- extension = options [ :extension ]
71
- country_code = options [ :country_code ]
72
-
73
- if area_code
74
- number . gsub! ( /(\d {1,3})(\d {3})(\d {4}$)/ , "(\\ 1) \\ 2#{ delimiter } \\ 3" )
75
- else
76
- number . gsub! ( /(\d {0,3})(\d {3})(\d {4})$/ , "\\ 1#{ delimiter } \\ 2#{ delimiter } \\ 3" )
77
- number . slice! ( 0 , 1 ) if number . start_with? ( delimiter ) && !delimiter . blank?
78
- end
79
-
80
- str = ''
81
- str << "+#{ country_code } #{ delimiter } " unless country_code . blank?
82
- str << number
83
- str << " x #{ extension } " unless extension . blank?
84
- ERB ::Util . html_escape ( str )
67
+ ERB ::Util . html_escape ( ActiveSupport ::NumberHelper . number_to_phone ( number , options ) )
85
68
end
86
69
87
70
# Formats a +number+ into a currency string (e.g., $13.65). You
@@ -128,34 +111,9 @@ def number_to_phone(number, options = {})
128
111
# # => 1234567890,50 £
129
112
def number_to_currency ( number , options = { } )
130
113
return unless number
131
- options = options . symbolize_keys
132
-
133
- currency = translations_for ( 'currency' , options [ :locale ] )
134
- currency [ :negative_format ] ||= "-" + currency [ :format ] if currency [ :format ]
135
-
136
- defaults = DEFAULT_CURRENCY_VALUES . merge ( defaults_translations ( options [ :locale ] ) ) . merge! ( currency )
137
- defaults [ :negative_format ] = "-" + options [ :format ] if options [ :format ]
138
- options = defaults . merge! ( options )
139
-
140
- unit = options . delete ( :unit )
141
- format = options . delete ( :format )
142
-
143
- if number . to_f < 0
144
- format = options . delete ( :negative_format )
145
- number = number . respond_to? ( "abs" ) ? number . abs : number . sub ( /^-/ , '' )
146
- end
147
-
148
- begin
149
- value = number_with_precision ( number , options . merge ( :raise => true ) )
150
- format . gsub ( '%n' , value ) . gsub ( '%u' , unit ) . html_safe
151
- rescue InvalidNumberError => e
152
- if options [ :raise ]
153
- raise
154
- else
155
- formatted_number = format . gsub ( '%n' , e . number ) . gsub ( '%u' , unit )
156
- e . number . to_s . html_safe? ? formatted_number . html_safe : formatted_number
157
- end
158
- end
114
+ options = escape_unsafe_delimiters_and_separators ( options . symbolize_keys )
115
+
116
+ wrap_with_output_safety_handling ( number , options [ :raise ] ) { ActiveSupport ::NumberHelper . number_to_currency ( number , options ) }
159
117
end
160
118
161
119
# Formats a +number+ as a percentage string (e.g., 65%). You can
@@ -196,24 +154,9 @@ def number_to_currency(number, options = {})
196
154
# number_to_percentage("98a", :raise => true) # => InvalidNumberError
197
155
def number_to_percentage ( number , options = { } )
198
156
return unless number
199
- options = options . symbolize_keys
200
-
201
- defaults = format_translations ( 'percentage' , options [ :locale ] )
202
- options = defaults . merge! ( options )
203
-
204
- format = options [ :format ] || "%n%"
205
-
206
- begin
207
- value = number_with_precision ( number , options . merge ( :raise => true ) )
208
- format . gsub ( /%n/ , value ) . html_safe
209
- rescue InvalidNumberError => e
210
- if options [ :raise ]
211
- raise
212
- else
213
- formatted_number = format . gsub ( /%n/ , e . number )
214
- e . number . to_s . html_safe? ? formatted_number . html_safe : formatted_number
215
- end
216
- end
157
+ options = escape_unsafe_delimiters_and_separators ( options . symbolize_keys )
158
+
159
+ wrap_with_output_safety_handling ( number , options [ :raise ] ) { ActiveSupport ::NumberHelper . number_to_percentage ( number , options ) }
217
160
end
218
161
219
162
# Formats a +number+ with grouped thousands using +delimiter+
@@ -246,15 +189,9 @@ def number_to_percentage(number, options = {})
246
189
#
247
190
# number_with_delimiter("112a", :raise => true) # => raise InvalidNumberError
248
191
def number_with_delimiter ( number , options = { } )
249
- options = options . symbolize_keys
250
-
251
- parse_float ( number , options [ :raise ] ) or return number
252
-
253
- options = defaults_translations ( options [ :locale ] ) . merge ( options )
192
+ options = escape_unsafe_delimiters_and_separators ( options . symbolize_keys )
254
193
255
- parts = number . to_s . to_str . split ( '.' )
256
- parts [ 0 ] . gsub! ( /(\d )(?=(\d \d \d )+(?!\d ))/ , "\\ 1#{ options [ :delimiter ] } " )
257
- safe_join ( parts , options [ :separator ] )
194
+ wrap_with_output_safety_handling ( number , options [ :raise ] ) { ActiveSupport ::NumberHelper . number_to_delimited ( number , options ) }
258
195
end
259
196
260
197
# Formats a +number+ with the specified level of
@@ -299,41 +236,11 @@ def number_with_delimiter(number, options = {})
299
236
# number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.')
300
237
# # => 1.111,23
301
238
def number_with_precision ( number , options = { } )
302
- options = options . symbolize_keys
303
-
304
- number = ( parse_float ( number , options [ :raise ] ) or return number )
305
-
306
- defaults = format_translations ( 'precision' , options [ :locale ] )
307
- options = defaults . merge! ( options )
308
-
309
- precision = options . delete :precision
310
- significant = options . delete :significant
311
- strip_insignificant_zeros = options . delete :strip_insignificant_zeros
239
+ options = escape_unsafe_delimiters_and_separators ( options . symbolize_keys )
312
240
313
- if significant and precision > 0
314
- if number == 0
315
- digits , rounded_number = 1 , 0
316
- else
317
- digits = ( Math . log10 ( number . abs ) + 1 ) . floor
318
- rounded_number = ( BigDecimal . new ( number . to_s ) / BigDecimal . new ( ( 10 ** ( digits - precision ) ) . to_f . to_s ) ) . round . to_f * 10 ** ( digits - precision )
319
- digits = ( Math . log10 ( rounded_number . abs ) + 1 ) . floor # After rounding, the number of digits may have changed
320
- end
321
- precision -= digits
322
- precision = precision > 0 ? precision : 0 #don't let it be negative
323
- else
324
- rounded_number = BigDecimal . new ( number . to_s ) . round ( precision ) . to_f
325
- rounded_number = rounded_number . zero? ? rounded_number . abs : rounded_number #prevent showing negative zeros
326
- end
327
- formatted_number = number_with_delimiter ( "%01.#{ precision } f" % rounded_number , options )
328
- if strip_insignificant_zeros
329
- escaped_separator = Regexp . escape ( options [ :separator ] )
330
- formatted_number . sub ( /(#{ escaped_separator } )(\d *[1-9])?0+\z / , '\1\2' ) . sub ( /#{ escaped_separator } \z / , '' ) . html_safe
331
- else
332
- formatted_number
333
- end
241
+ wrap_with_output_safety_handling ( number , options [ :raise ] ) { ActiveSupport ::NumberHelper . number_to_rounded ( number , options ) }
334
242
end
335
243
336
- STORAGE_UNITS = [ :byte , :kb , :mb , :gb , :tb ] . freeze
337
244
338
245
# Formats the bytes in +number+ into a more understandable
339
246
# representation (e.g., giving it 1500 yields 1.5 KB). This
@@ -383,40 +290,11 @@ def number_with_precision(number, options = {})
383
290
# number_to_human_size(1234567890123, :precision => 5) # => "1.1229 TB"
384
291
# number_to_human_size(524288000, :precision => 5) # => "500 MB"
385
292
def number_to_human_size ( number , options = { } )
386
- options = options . symbolize_keys
387
-
388
- number = ( parse_float ( number , options [ :raise ] ) or return number )
389
-
390
- defaults = format_translations ( 'human' , options [ :locale ] )
391
- options = defaults . merge! ( options )
392
-
393
- #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
394
- options [ :strip_insignificant_zeros ] = true if not options . key? ( :strip_insignificant_zeros )
395
-
396
- storage_units_format = I18n . translate ( :'number.human.storage_units.format' , :locale => options [ :locale ] , :raise => true )
397
-
398
- base = options [ :prefix ] == :si ? 1000 : 1024
293
+ options = escape_unsafe_delimiters_and_separators ( options . symbolize_keys )
399
294
400
- if number . to_i < base
401
- unit = I18n . translate ( :'number.human.storage_units.units.byte' , :locale => options [ :locale ] , :count => number . to_i , :raise => true )
402
- storage_units_format . gsub ( /%n/ , number . to_i . to_s ) . gsub ( /%u/ , unit ) . html_safe
403
- else
404
- max_exp = STORAGE_UNITS . size - 1
405
- exponent = ( Math . log ( number ) / Math . log ( base ) ) . to_i # Convert to base
406
- exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
407
- number /= base ** exponent
408
-
409
- unit_key = STORAGE_UNITS [ exponent ]
410
- unit = I18n . translate ( :"number.human.storage_units.units.#{ unit_key } " , :locale => options [ :locale ] , :count => number , :raise => true )
411
-
412
- formatted_number = number_with_precision ( number , options )
413
- storage_units_format . gsub ( /%n/ , formatted_number ) . gsub ( /%u/ , unit ) . html_safe
414
- end
295
+ wrap_with_output_safety_handling ( number , options [ :raise ] ) { ActiveSupport ::NumberHelper . number_to_human_size ( number , options ) }
415
296
end
416
297
417
- DECIMAL_UNITS = { 0 => :unit , 1 => :ten , 2 => :hundred , 3 => :thousand , 6 => :million , 9 => :billion , 12 => :trillion , 15 => :quadrillion ,
418
- -1 => :deci , -2 => :centi , -3 => :mili , -6 => :micro , -9 => :nano , -12 => :pico , -15 => :femto } . freeze
419
-
420
298
# Pretty prints (formats and approximates) a number in a way it
421
299
# is more readable by humans (eg.: 1200000000 becomes "1.2
422
300
# Billion"). This is useful for numbers that can get very large
@@ -516,60 +394,33 @@ def number_to_human_size(number, options = {})
516
394
# number_to_human(0.34, :units => :distance) # => "34 centimeters"
517
395
#
518
396
def number_to_human ( number , options = { } )
519
- options = options . symbolize_keys
520
-
521
- number = ( parse_float ( number , options [ :raise ] ) or return number )
397
+ options = escape_unsafe_delimiters_and_separators ( options . symbolize_keys )
522
398
523
- defaults = format_translations ( 'human' , options [ :locale ] )
524
- options = defaults . merge! ( options )
525
-
526
- #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
527
- options [ :strip_insignificant_zeros ] = true if not options . key? ( :strip_insignificant_zeros )
528
-
529
- inverted_du = DECIMAL_UNITS . invert
530
-
531
- units = options . delete :units
532
- unit_exponents = case units
533
- when Hash
534
- units
535
- when String , Symbol
536
- I18n . translate ( :"#{ units } " , :locale => options [ :locale ] , :raise => true )
537
- when nil
538
- I18n . translate ( :"number.human.decimal_units.units" , :locale => options [ :locale ] , :raise => true )
539
- else
540
- raise ArgumentError , ":units must be a Hash or String translation scope."
541
- end . keys . map { |e_name | inverted_du [ e_name ] } . sort_by { |e | -e }
542
-
543
- number_exponent = number != 0 ? Math . log10 ( number . abs ) . floor : 0
544
- display_exponent = unit_exponents . find { |e | number_exponent >= e } || 0
545
- number /= 10 ** display_exponent
546
-
547
- unit = case units
548
- when Hash
549
- units [ DECIMAL_UNITS [ display_exponent ] ]
550
- when String , Symbol
551
- I18n . translate ( :"#{ units } .#{ DECIMAL_UNITS [ display_exponent ] } " , :locale => options [ :locale ] , :count => number . to_i )
552
- else
553
- I18n . translate ( :"number.human.decimal_units.units.#{ DECIMAL_UNITS [ display_exponent ] } " , :locale => options [ :locale ] , :count => number . to_i )
554
- end
555
-
556
- decimal_format = options [ :format ] || I18n . translate ( :'number.human.decimal_units.format' , :locale => options [ :locale ] , :default => "%n %u" )
557
- formatted_number = number_with_precision ( number , options )
558
- decimal_format . gsub ( /%n/ , formatted_number ) . gsub ( /%u/ , unit ) . strip . html_safe
399
+ wrap_with_output_safety_handling ( number , options [ :raise ] ) { ActiveSupport ::NumberHelper . number_to_human ( number , options ) }
559
400
end
560
401
561
402
private
562
-
563
- def format_translations ( namespace , locale )
564
- defaults_translations ( locale ) . merge ( translations_for ( namespace , locale ) )
403
+
404
+ def escape_unsafe_delimiters_and_separators ( options )
405
+ options [ :separator ] = ERB ::Util . html_escape ( options [ :separator ] ) if options [ :separator ] && !options [ :separator ] . html_safe?
406
+ options [ :delimiter ] = ERB ::Util . html_escape ( options [ :delimiter ] ) if options [ :delimiter ] && !options [ :delimiter ] . html_safe?
407
+ options
565
408
end
566
-
567
- def defaults_translations ( locale )
568
- I18n . translate ( :'number.format' , :locale => locale , :default => { } )
409
+
410
+ def wrap_with_output_safety_handling ( number , raise_on_invalid , &block )
411
+ raise InvalidNumberError , number if raise_on_invalid && !valid_float? ( number )
412
+
413
+ formatted_number = yield
414
+
415
+ if valid_float? ( number ) || number . html_safe?
416
+ formatted_number . html_safe
417
+ else
418
+ formatted_number
419
+ end
569
420
end
570
-
571
- def translations_for ( namespace , locale )
572
- I18n . translate ( :" number. #{ namespace } .format" , :locale => locale , :default => { } )
421
+
422
+ def valid_float? ( number )
423
+ ! parse_float ( number , false ) . nil?
573
424
end
574
425
575
426
def parse_float ( number , raise_error )
0 commit comments