diff options
-rw-r--r-- | actionpack/lib/action_view/helpers/number_helper.rb | 225 | ||||
-rw-r--r-- | actionpack/lib/action_view/locale/en.yml | 98 | ||||
-rw-r--r-- | actionpack/test/template/number_helper_test.rb | 11 | ||||
-rw-r--r-- | activesupport/CHANGELOG.md | 4 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/big_decimal/conversions.rb | 9 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/numeric.rb | 1 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/numeric/conversions.rb | 135 | ||||
-rw-r--r-- | activesupport/lib/active_support/locale/en.yml | 99 | ||||
-rw-r--r-- | activesupport/lib/active_support/number_helper.rb | 532 | ||||
-rw-r--r-- | activesupport/test/core_ext/bigdecimal_test.rb | 5 | ||||
-rw-r--r-- | activesupport/test/core_ext/numeric_ext_test.rb | 261 | ||||
-rw-r--r-- | activesupport/test/number_helper_test.rb | 373 | ||||
-rw-r--r-- | guides/source/active_support_core_extensions.textile | 70 |
13 files changed, 1536 insertions, 287 deletions
diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index dfc26acfad..b743f5e0c9 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -2,12 +2,16 @@ require 'active_support/core_ext/big_decimal/conversions' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/numeric' require 'active_support/core_ext/string/output_safety' +require 'active_support/number_helper' +require 'erb' module ActionView # = Action View Number Helpers module Helpers #:nodoc: + # Provides methods for converting numbers into formatted strings. # Methods are provided for phone numbers, currency, percentage, # precision, positional notation, file size and pretty printing. @@ -16,9 +20,6 @@ module ActionView # unchanged if can't be converted into a valid number. module NumberHelper - DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :negative_format => "-%u%n", :unit => "$", :separator => ".", :delimiter => ",", - :precision => 2, :significant => false, :strip_insignificant_zeros => false } - # Raised when argument +number+ param given to the helpers is invalid and # the option :raise is set to +true+. class InvalidNumberError < StandardError @@ -63,25 +64,7 @@ module ActionView options = options.symbolize_keys parse_float(number, true) if options[:raise] - - number = number.to_s.strip - area_code = options[:area_code] - delimiter = options[:delimiter] || "-" - extension = options[:extension] - country_code = options[:country_code] - - if area_code - number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3") - else - number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") - number.slice!(0, 1) if number.start_with?(delimiter) && !delimiter.blank? - end - - str = '' - str << "+#{country_code}#{delimiter}" unless country_code.blank? - str << number - str << " x #{extension}" unless extension.blank? - ERB::Util.html_escape(str) + ERB::Util.html_escape(ActiveSupport::NumberHelper.number_to_phone(number, options)) end # Formats a +number+ into a currency string (e.g., $13.65). You @@ -128,34 +111,9 @@ module ActionView # # => 1234567890,50 £ def number_to_currency(number, options = {}) return unless number - options = options.symbolize_keys - - currency = translations_for('currency', options[:locale]) - currency[:negative_format] ||= "-" + currency[:format] if currency[:format] - - defaults = DEFAULT_CURRENCY_VALUES.merge(defaults_translations(options[:locale])).merge!(currency) - defaults[:negative_format] = "-" + options[:format] if options[:format] - options = defaults.merge!(options) - - unit = options.delete(:unit) - format = options.delete(:format) - - if number.to_f < 0 - format = options.delete(:negative_format) - number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '') - end - - begin - value = number_with_precision(number, options.merge(:raise => true)) - format.gsub('%n', value).gsub('%u', unit).html_safe - rescue InvalidNumberError => e - if options[:raise] - raise - else - formatted_number = format.gsub('%n', e.number).gsub('%u', unit) - e.number.to_s.html_safe? ? formatted_number.html_safe : formatted_number - end - end + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) + + wrap_with_output_safety_handling(number, options[:raise]){ ActiveSupport::NumberHelper.number_to_currency(number, options) } end # Formats a +number+ as a percentage string (e.g., 65%). You can @@ -196,24 +154,9 @@ module ActionView # number_to_percentage("98a", :raise => true) # => InvalidNumberError def number_to_percentage(number, options = {}) return unless number - options = options.symbolize_keys - - defaults = format_translations('percentage', options[:locale]) - options = defaults.merge!(options) - - format = options[:format] || "%n%" - - begin - value = number_with_precision(number, options.merge(:raise => true)) - format.gsub(/%n/, value).html_safe - rescue InvalidNumberError => e - if options[:raise] - raise - else - formatted_number = format.gsub(/%n/, e.number) - e.number.to_s.html_safe? ? formatted_number.html_safe : formatted_number - end - end + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) + + wrap_with_output_safety_handling(number, options[:raise]){ ActiveSupport::NumberHelper.number_to_percentage(number, options) } end # Formats a +number+ with grouped thousands using +delimiter+ @@ -246,15 +189,9 @@ module ActionView # # number_with_delimiter("112a", :raise => true) # => raise InvalidNumberError def number_with_delimiter(number, options = {}) - options = options.symbolize_keys - - parse_float(number, options[:raise]) or return number - - options = defaults_translations(options[:locale]).merge(options) + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - parts = number.to_s.to_str.split('.') - parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") - safe_join(parts, options[:separator]) + wrap_with_output_safety_handling(number, options[:raise]){ ActiveSupport::NumberHelper.number_to_delimited(number, options) } end # Formats a +number+ with the specified level of @@ -299,41 +236,11 @@ module ActionView # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.') # # => 1.111,23 def number_with_precision(number, options = {}) - options = options.symbolize_keys - - number = (parse_float(number, options[:raise]) or return number) - - defaults = format_translations('precision', options[:locale]) - options = defaults.merge!(options) - - precision = options.delete :precision - significant = options.delete :significant - strip_insignificant_zeros = options.delete :strip_insignificant_zeros + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - if significant and precision > 0 - if number == 0 - digits, rounded_number = 1, 0 - else - digits = (Math.log10(number.abs) + 1).floor - rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new((10 ** (digits - precision)).to_f.to_s)).round.to_f * 10 ** (digits - precision) - digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed - end - precision -= digits - precision = precision > 0 ? precision : 0 #don't let it be negative - else - rounded_number = BigDecimal.new(number.to_s).round(precision).to_f - rounded_number = rounded_number.zero? ? rounded_number.abs : rounded_number #prevent showing negative zeros - end - formatted_number = number_with_delimiter("%01.#{precision}f" % rounded_number, options) - if strip_insignificant_zeros - escaped_separator = Regexp.escape(options[:separator]) - formatted_number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '').html_safe - else - formatted_number - end + wrap_with_output_safety_handling(number, options[:raise]){ ActiveSupport::NumberHelper.number_to_rounded(number, options) } end - STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb].freeze # Formats the bytes in +number+ into a more understandable # representation (e.g., giving it 1500 yields 1.5 KB). This @@ -383,40 +290,11 @@ module ActionView # number_to_human_size(1234567890123, :precision => 5) # => "1.1229 TB" # number_to_human_size(524288000, :precision => 5) # => "500 MB" def number_to_human_size(number, options = {}) - options = options.symbolize_keys - - number = (parse_float(number, options[:raise]) or return number) - - defaults = format_translations('human', options[:locale]) - options = defaults.merge!(options) - - #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files - options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) - - storage_units_format = I18n.translate(:'number.human.storage_units.format', :locale => options[:locale], :raise => true) - - base = options[:prefix] == :si ? 1000 : 1024 + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - if number.to_i < base - unit = I18n.translate(:'number.human.storage_units.units.byte', :locale => options[:locale], :count => number.to_i, :raise => true) - storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit).html_safe - else - max_exp = STORAGE_UNITS.size - 1 - exponent = (Math.log(number) / Math.log(base)).to_i # Convert to base - exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit - number /= base ** exponent - - unit_key = STORAGE_UNITS[exponent] - unit = I18n.translate(:"number.human.storage_units.units.#{unit_key}", :locale => options[:locale], :count => number, :raise => true) - - formatted_number = number_with_precision(number, options) - storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).html_safe - end + wrap_with_output_safety_handling(number, options[:raise]){ ActiveSupport::NumberHelper.number_to_human_size(number, options) } end - DECIMAL_UNITS = {0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion, - -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto}.freeze - # Pretty prints (formats and approximates) a number in a way it # is more readable by humans (eg.: 1200000000 becomes "1.2 # Billion"). This is useful for numbers that can get very large @@ -516,60 +394,33 @@ module ActionView # number_to_human(0.34, :units => :distance) # => "34 centimeters" # def number_to_human(number, options = {}) - options = options.symbolize_keys - - number = (parse_float(number, options[:raise]) or return number) + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - defaults = format_translations('human', options[:locale]) - options = defaults.merge!(options) - - #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files - options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) - - inverted_du = DECIMAL_UNITS.invert - - units = options.delete :units - unit_exponents = case units - when Hash - units - when String, Symbol - I18n.translate(:"#{units}", :locale => options[:locale], :raise => true) - when nil - I18n.translate(:"number.human.decimal_units.units", :locale => options[:locale], :raise => true) - else - raise ArgumentError, ":units must be a Hash or String translation scope." - end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e} - - number_exponent = number != 0 ? Math.log10(number.abs).floor : 0 - display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0 - number /= 10 ** display_exponent - - unit = case units - when Hash - units[DECIMAL_UNITS[display_exponent]] - when String, Symbol - I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) - else - I18n.translate(:"number.human.decimal_units.units.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) - end - - decimal_format = options[:format] || I18n.translate(:'number.human.decimal_units.format', :locale => options[:locale], :default => "%n %u") - formatted_number = number_with_precision(number, options) - decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip.html_safe + wrap_with_output_safety_handling(number, options[:raise]){ ActiveSupport::NumberHelper.number_to_human(number, options) } end private - - def format_translations(namespace, locale) - defaults_translations(locale).merge(translations_for(namespace, locale)) + + def escape_unsafe_delimiters_and_separators(options) + options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator] && !options[:separator].html_safe? + options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter] && !options[:delimiter].html_safe? + options end - - def defaults_translations(locale) - I18n.translate(:'number.format', :locale => locale, :default => {}) + + def wrap_with_output_safety_handling(number, raise_on_invalid, &block) + raise InvalidNumberError, number if raise_on_invalid && !valid_float?(number) + + formatted_number = yield + + if valid_float?(number) || number.html_safe? + formatted_number.html_safe + else + formatted_number + end end - - def translations_for(namespace, locale) - I18n.translate(:"number.#{namespace}.format", :locale => locale, :default => {}) + + def valid_float?(number) + !parse_float(number, false).nil? end def parse_float(number, raise_error) diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml index 8e9db634fb..8a56f147b8 100644 --- a/actionpack/lib/action_view/locale/en.yml +++ b/actionpack/lib/action_view/locale/en.yml @@ -1,102 +1,4 @@ "en": - number: - # Used in number_with_delimiter() - # These are also the defaults for 'currency', 'percentage', 'precision', and 'human' - format: - # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5) - separator: "." - # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three) - delimiter: "," - # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00) - precision: 3 - # If set to true, precision will mean the number of significant digits instead - # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2) - significant: false - # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2) - strip_insignificant_zeros: false - - # Used in number_to_currency() - currency: - format: - # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00) - format: "%u%n" - unit: "$" - # These five are to override number.format and are optional - separator: "." - delimiter: "," - precision: 2 - significant: false - strip_insignificant_zeros: false - - # Used in number_to_percentage() - percentage: - format: - # These five are to override number.format and are optional - # separator: - delimiter: "" - # precision: - # significant: false - # strip_insignificant_zeros: false - format: "%n%" - - # Used in number_to_precision() - precision: - format: - # These five are to override number.format and are optional - # separator: - delimiter: "" - # precision: - # significant: false - # strip_insignificant_zeros: false - - # Used in number_to_human_size() and number_to_human() - human: - format: - # These five are to override number.format and are optional - # separator: - delimiter: "" - precision: 3 - significant: true - strip_insignificant_zeros: true - # Used in number_to_human_size() - storage_units: - # Storage units output formatting. - # %u is the storage unit, %n is the number (default: 2 MB) - format: "%n %u" - units: - byte: - one: "Byte" - other: "Bytes" - kb: "KB" - mb: "MB" - gb: "GB" - tb: "TB" - # Used in number_to_human() - decimal_units: - format: "%n %u" - # Decimal units output formatting - # By default we will only quantify some of the exponents - # but the commented ones might be defined or overridden - # by the user. - units: - # femto: Quadrillionth - # pico: Trillionth - # nano: Billionth - # micro: Millionth - # mili: Thousandth - # centi: Hundredth - # deci: Tenth - unit: "" - # ten: - # one: Ten - # other: Tens - # hundred: Hundred - thousand: Thousand - million: Million - billion: Billion - trillion: Trillion - quadrillion: Quadrillion - # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words() datetime: distance_in_words: diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb index 14ca6d9879..057cb47f53 100644 --- a/actionpack/test/template/number_helper_test.rb +++ b/actionpack/test/template/number_helper_test.rb @@ -33,6 +33,7 @@ class NumberHelperTest < ActionView::TestCase assert_equal("+18005551212", number_to_phone(8005551212, :country_code => 1, :delimiter => '')) assert_equal("22-555-1212", number_to_phone(225551212)) assert_equal("+45-22-555-1212", number_to_phone(225551212, :country_code => 45)) + assert_equal '111<script></script>111<script></script>1111', number_to_phone(1111111111, :delimiter => "<script></script>") end def test_number_to_currency @@ -47,6 +48,8 @@ class NumberHelperTest < ActionView::TestCase assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50")) assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"})) assert_equal("1,234,567,890.50 - Kč", number_to_currency("-1234567890.50", {:unit => "Kč", :format => "%n %u", :negative_format => "%n - %u"})) + assert_equal '$1<script></script>01', number_to_currency(1.01, :separator => "<script></script>") + assert_equal '$1<script></script>000.00', number_to_currency(1000, :delimiter => "<script></script>") end def test_number_to_percentage @@ -58,6 +61,8 @@ class NumberHelperTest < ActionView::TestCase assert_equal("123.4%", number_to_percentage(123.400, :precision => 3, :strip_insignificant_zeros => true)) assert_equal("1.000,000%", number_to_percentage(1000, :delimiter => '.', :separator => ',')) assert_equal("1000.000 %", number_to_percentage(1000, :format => "%n %")) + assert_equal '1<script></script>010%', number_to_percentage(1.01, :separator => "<script></script>") + assert_equal '1<script></script>000.000%', number_to_percentage(1000, :delimiter => "<script></script>") end def test_number_with_delimiter @@ -104,6 +109,8 @@ class NumberHelperTest < ActionView::TestCase def test_number_with_precision_with_custom_delimiter_and_separator assert_equal '31,83', number_with_precision(31.825, :precision => 2, :separator => ',') assert_equal '1.231,83', number_with_precision(1231.825, :precision => 2, :separator => ',', :delimiter => '.') + assert_equal '1<script></script>010', number_with_precision(1.01, :separator => "<script></script>") + assert_equal '1<script></script>000.000', number_with_precision(1000, :delimiter => "<script></script>") end def test_number_with_precision_with_significant_digits @@ -193,6 +200,7 @@ class NumberHelperTest < ActionView::TestCase assert_equal '1.0 KB', number_to_human_size(kilobytes(1.0123), :precision => 2, :strip_insignificant_zeros => false) assert_equal '1.012 KB', number_to_human_size(kilobytes(1.0123), :precision => 3, :significant => false) assert_equal '1 KB', number_to_human_size(kilobytes(1.0123), :precision => 0, :significant => true) #ignores significant it precision is 0 + assert_equal '9<script></script>86 KB', number_to_human_size(10100, :separator => "<script></script>") end def test_number_to_human_size_with_custom_delimiter_and_separator @@ -253,6 +261,9 @@ class NumberHelperTest < ActionView::TestCase #Spaces are stripped from the resulting string assert_equal '4', number_to_human(4, :units => {:unit => "", :ten => 'tens '}) assert_equal '4.5 tens', number_to_human(45, :units => {:unit => "", :ten => ' tens '}) + + assert_equal '1<script></script>01', number_to_human(1.01, :separator => "<script></script>") + assert_equal '100<script></script>000 Quadrillion', number_to_human(10**20, :delimiter => "<script></script>") end def test_number_to_human_with_custom_format diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 62b8a789c7..a818ef0c5d 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,9 @@ ## Rails 4.0.0 (unreleased) ## +* ActionView::Helpers::NumberHelper methods have been moved to ActiveSupport::NumberHelper and are now available via + Numeric#to_s. Numeric#to_s now accepts the formatting options :phone, :currency, :percentage, :delimited, + :rounded, :human, and :human_size. *Andrew Mutz* + * Add `Hash#transform_keys`, `Hash#transform_keys!`, `Hash#deep_transform_keys`, and `Hash#deep_transform_keys!`. *Mark McSpadden* * Changed xml type `datetime` to `dateTime` (with upper case letter `T`). *Angelo Capilleri* diff --git a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb index 3ec7e576c8..5dc5710c53 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb @@ -17,8 +17,13 @@ class BigDecimal end DEFAULT_STRING_FORMAT = 'F' - def to_formatted_s(format = DEFAULT_STRING_FORMAT) - _original_to_s(format) + def to_formatted_s(*args) + if args[0].is_a?(Symbol) + super + else + format = args[0] || DEFAULT_STRING_FORMAT + _original_to_s(format) + end end alias_method :_original_to_s, :to_s alias_method :to_s, :to_formatted_s diff --git a/activesupport/lib/active_support/core_ext/numeric.rb b/activesupport/lib/active_support/core_ext/numeric.rb index 3805cf7990..a6bc0624be 100644 --- a/activesupport/lib/active_support/core_ext/numeric.rb +++ b/activesupport/lib/active_support/core_ext/numeric.rb @@ -1,2 +1,3 @@ require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' +require 'active_support/core_ext/numeric/conversions' diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb new file mode 100644 index 0000000000..2666387623 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -0,0 +1,135 @@ +require 'active_support/core_ext/big_decimal/conversions' +require 'active_support/number_helper' + +class Numeric + + # Provides options for converting numbers into formatted strings. + # Options are provided for phone numbers, currency, percentage, + # precision, positional notation, file size and pretty printing. + # + # ==== Options + # + # For details on which formats use which options, see ActiveSupport::NumberHelper + # + # ==== Examples + # + # Phone Numbers: + # 5551234.to_s(:phone) # => 555-1234 + # 1235551234.to_s(:phone) # => 123-555-1234 + # 1235551234.to_s(:phone, :area_code => true) # => (123) 555-1234 + # 1235551234.to_s(:phone, :delimiter => " ") # => 123 555 1234 + # 1235551234.to_s(:phone, :area_code => true, :extension => 555) # => (123) 555-1234 x 555 + # 1235551234.to_s(:phone, :country_code => 1) # => +1-123-555-1234 + # 1235551234.to_s(:phone, :country_code => 1, :extension => 1343, :delimiter => ".") + # # => +1.123.555.1234 x 1343 + # + # Currency: + # 1234567890.50.to_s(:currency) # => $1,234,567,890.50 + # 1234567890.506.to_s(:currency) # => $1,234,567,890.51 + # 1234567890.506.to_s(:currency, :precision => 3) # => $1,234,567,890.506 + # 1234567890.506.to_s(:currency, :locale => :fr) # => 1 234 567 890,51 € + # -1234567890.50.to_s(:currency, :negative_format => "(%u%n)") + # # => ($1,234,567,890.50) + # 1234567890.50.to_s(:currency, :unit => "£", :separator => ",", :delimiter => "") + # # => £1234567890,50 + # 1234567890.50.to_s(:currency, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") + # # => 1234567890,50 £ + # + # Percentage: + # 100.to_s(:percentage) # => 100.000% + # 100.to_s(:percentage, :precision => 0) # => 100% + # 1000.to_s(:percentage, :delimiter => '.', :separator => ',') # => 1.000,000% + # 302.24398923423.to_s(:percentage, :precision => 5) # => 302.24399% + # 1000.to_s(:percentage, :locale => :fr) # => 1 000,000% + # 100.to_s(:percentage, :format => "%n %") # => 100 % + # + # Delimited: + # 12345678.to_s(:delimited) # => 12,345,678 + # 12345678.05.to_s(:delimited) # => 12,345,678.05 + # 12345678.to_s(:delimited, :delimiter => ".") # => 12.345.678 + # 12345678.to_s(:delimited, :delimiter => ",") # => 12,345,678 + # 12345678.05.to_s(:delimited, :separator => " ") # => 12,345,678 05 + # 12345678.05.to_s(:delimited, :locale => :fr) # => 12 345 678,05 + # 98765432.98.to_s(:delimited, :delimiter => " ", :separator => ",") + # # => 98 765 432,98 + # + # Rounded: + # 111.2345.to_s(:rounded) # => 111.235 + # 111.2345.to_s(:rounded, :precision => 2) # => 111.23 + # 13.to_s(:rounded, :precision => 5) # => 13.00000 + # 389.32314.to_s(:rounded, :precision => 0) # => 389 + # 111.2345.to_s(:rounded, :significant => true) # => 111 + # 111.2345.to_s(:rounded, :precision => 1, :significant => true) # => 100 + # 13.to_s(:rounded, :precision => 5, :significant => true) # => 13.000 + # 111.234.to_s(:rounded, :locale => :fr) # => 111,234 + # 13.to_s(:rounded, :precision => 5, :significant => true, :strip_insignificant_zeros => true) + # # => 13 + # 389.32314.to_s(:rounded, :precision => 4, :significant => true) # => 389.3 + # 1111.2345.to_s(:rounded, :precision => 2, :separator => ',', :delimiter => '.') + # # => 1.111,23 + # + # Human-friendly size in Bytes: + # 123.to_s(:human_size) # => 123 Bytes + # 1234.to_s(:human_size) # => 1.21 KB + # 12345.to_s(:human_size) # => 12.1 KB + # 1234567.to_s(:human_size) # => 1.18 MB + # 1234567890.to_s(:human_size) # => 1.15 GB + # 1234567890123.to_s(:human_size) # => 1.12 TB + # 1234567.to_s(:human_size, :precision => 2) # => 1.2 MB + # 483989.to_s(:human_size, :precision => 2) # => 470 KB + # 1234567.to_s(:human_size, :precision => 2, :separator => ',') # => 1,2 MB + # 1234567890123.to_s(:human_size, :precision => 5) # => "1.1229 TB" + # 524288000.to_s(:human_size, :precision => 5) # => "500 MB" + # + # Human-friendly format: + # 123.to_s(:human) # => "123" + # 1234.to_s(:human) # => "1.23 Thousand" + # 12345.to_s(:human) # => "12.3 Thousand" + # 1234567.to_s(:human) # => "1.23 Million" + # 1234567890.to_s(:human) # => "1.23 Billion" + # 1234567890123.to_s(:human) # => "1.23 Trillion" + # 1234567890123456.to_s(:human) # => "1.23 Quadrillion" + # 1234567890123456789.to_s(:human) # => "1230 Quadrillion" + # 489939.to_s(:human, :precision => 2) # => "490 Thousand" + # 489939.to_s(:human, :precision => 4) # => "489.9 Thousand" + # 1234567.to_s(:human, :precision => 4, + # :significant => false) # => "1.2346 Million" + # 1234567.to_s(:human, :precision => 1, + # :separator => ',', + # :significant => false) # => "1,2 Million" + def to_formatted_s(format = :default, options = {}) + case format + when :phone + return ActiveSupport::NumberHelper.number_to_phone(self, options) + when :currency + return ActiveSupport::NumberHelper.number_to_currency(self, options) + when :percentage + return ActiveSupport::NumberHelper.number_to_percentage(self, options) + when :delimited + return ActiveSupport::NumberHelper.number_to_delimited(self, options) + when :rounded + return ActiveSupport::NumberHelper.number_to_rounded(self, options) + when :human + return ActiveSupport::NumberHelper.number_to_human(self, options) + when :human_size + return ActiveSupport::NumberHelper.number_to_human_size(self, options) + else + self.to_default_s + end + end + + [Float, Fixnum, Bignum, BigDecimal].each do |klass| + klass.send(:alias_method, :to_default_s, :to_s) + + klass.send(:define_method, :to_s) do |*args| + if args[0].is_a?(Symbol) + format = args[0] + options = args[1] || {} + + self.to_formatted_s(format, options) + else + to_default_s(*args) + end + end + end +end
\ No newline at end of file diff --git a/activesupport/lib/active_support/locale/en.yml b/activesupport/lib/active_support/locale/en.yml index a1499bcc90..18c7d47026 100644 --- a/activesupport/lib/active_support/locale/en.yml +++ b/activesupport/lib/active_support/locale/en.yml @@ -34,3 +34,102 @@ en: words_connector: ", " two_words_connector: " and " last_word_connector: ", and " + number: + # Used in NumberHelper.number_to_delimited() + # These are also the defaults for 'currency', 'percentage', 'precision', and 'human' + format: + # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5) + separator: "." + # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three) + delimiter: "," + # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00) + precision: 3 + # If set to true, precision will mean the number of significant digits instead + # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2) + significant: false + # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2) + strip_insignificant_zeros: false + + # Used in NumberHelper.number_to_currency() + currency: + format: + # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00) + format: "%u%n" + unit: "$" + # These five are to override number.format and are optional + separator: "." + delimiter: "," + precision: 2 + significant: false + strip_insignificant_zeros: false + + # Used in NumberHelper.number_to_percentage() + percentage: + format: + # These five are to override number.format and are optional + # separator: + delimiter: "" + # precision: + # significant: false + # strip_insignificant_zeros: false + format: "%n%" + + # Used in NumberHelper.number_to_rounded() + precision: + format: + # These five are to override number.format and are optional + # separator: + delimiter: "" + # precision: + # significant: false + # strip_insignificant_zeros: false + + # Used in NumberHelper.number_to_human_size() and NumberHelper.number_to_human() + human: + format: + # These five are to override number.format and are optional + # separator: + delimiter: "" + precision: 3 + significant: true + strip_insignificant_zeros: true + # Used in number_to_human_size() + storage_units: + # Storage units output formatting. + # %u is the storage unit, %n is the number (default: 2 MB) + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + # Used in NumberHelper.number_to_human() + decimal_units: + format: "%n %u" + # Decimal units output formatting + # By default we will only quantify some of the exponents + # but the commented ones might be defined or overridden + # by the user. + units: + # femto: Quadrillionth + # pico: Trillionth + # nano: Billionth + # micro: Millionth + # mili: Thousandth + # centi: Hundredth + # deci: Tenth + unit: "" + # ten: + # one: Ten + # other: Tens + # hundred: Hundred + thousand: Thousand + million: Million + billion: Billion + trillion: Trillion + quadrillion: Quadrillion + +
\ No newline at end of file diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb new file mode 100644 index 0000000000..8f5309c824 --- /dev/null +++ b/activesupport/lib/active_support/number_helper.rb @@ -0,0 +1,532 @@ +require 'active_support/core_ext/big_decimal/conversions' +require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/keys' +require 'active_support/i18n' + +module ActiveSupport + module NumberHelper + extend self + + DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :negative_format => "-%u%n", :unit => "$", :separator => ".", :delimiter => ",", + :precision => 2, :significant => false, :strip_insignificant_zeros => false } + + # Formats a +number+ into a US phone number (e.g., (555) + # 123-9876). You can customize the format in the +options+ hash. + # + # ==== Options + # + # * <tt>:area_code</tt> - Adds parentheses around the area code. + # * <tt>:delimiter</tt> - Specifies the delimiter to use + # (defaults to "-"). + # * <tt>:extension</tt> - Specifies an extension to add to the + # end of the generated number. + # * <tt>:country_code</tt> - Sets the country code for the phone + # number. + # ==== Examples + # + # number_to_phone(5551234) # => 555-1234 + # number_to_phone("5551234") # => 555-1234 + # number_to_phone(1235551234) # => 123-555-1234 + # number_to_phone(1235551234, :area_code => true) # => (123) 555-1234 + # number_to_phone(1235551234, :delimiter => " ") # => 123 555 1234 + # number_to_phone(1235551234, :area_code => true, :extension => 555) # => (123) 555-1234 x 555 + # number_to_phone(1235551234, :country_code => 1) # => +1-123-555-1234 + # number_to_phone("123a456") # => 123a456 + # + # number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".") + # # => +1.123.555.1234 x 1343 + def number_to_phone(number, options = {}) + return unless number + options = options.symbolize_keys + + number = number.to_s.strip + area_code = options[:area_code] + delimiter = options[:delimiter] || "-" + extension = options[:extension] + country_code = options[:country_code] + + if area_code + number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3") + else + number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") + number.slice!(0, 1) if number.start_with?(delimiter) && !delimiter.blank? + end + + str = '' + str << "+#{country_code}#{delimiter}" unless country_code.blank? + str << number + str << " x #{extension}" unless extension.blank? + str + end + + # Formats a +number+ into a currency string (e.g., $13.65). You + # can customize the format in the +options+ hash. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the level of precision (defaults + # to 2). + # * <tt>:unit</tt> - Sets the denomination of the currency + # (defaults to "$"). + # * <tt>:separator</tt> - Sets the separator between the units + # (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ","). + # * <tt>:format</tt> - Sets the format for non-negative numbers + # (defaults to "%u%n"). Fields are <tt>%u</tt> for the + # currency, and <tt>%n</tt> for the number. + # * <tt>:negative_format</tt> - Sets the format for negative + # numbers (defaults to prepending an hyphen to the formatted + # number given by <tt>:format</tt>). Accepts the same fields + # than <tt>:format</tt>, except <tt>%n</tt> is here the + # absolute value of the number. + # + # ==== Examples + # + # number_to_currency(1234567890.50) # => $1,234,567,890.50 + # number_to_currency(1234567890.506) # => $1,234,567,890.51 + # number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506 + # number_to_currency(1234567890.506, :locale => :fr) # => 1 234 567 890,51 € + # number_to_currency("123a456") # => $123a456 + # + # number_to_currency(-1234567890.50, :negative_format => "(%u%n)") + # # => ($1,234,567,890.50) + # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "") + # # => £1234567890,50 + # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") + # # => 1234567890,50 £ + def number_to_currency(number, options = {}) + return unless number + options = options.symbolize_keys + + currency = translations_for('currency', options[:locale]) + currency[:negative_format] ||= "-" + currency[:format] if currency[:format] + + defaults = DEFAULT_CURRENCY_VALUES.merge(defaults_translations(options[:locale])).merge!(currency) + defaults[:negative_format] = "-" + options[:format] if options[:format] + options = defaults.merge!(options) + + unit = options.delete(:unit) + format = options.delete(:format) + + if number.to_f < 0 + format = options.delete(:negative_format) + number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '') + end + + formatted_number = format.gsub('%n', self.number_to_rounded(number, options)).gsub('%u', unit) + formatted_number + end + + # Formats a +number+ as a percentage string (e.g., 65%). You can + # customize the format in the +options+ hash. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number + # (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # + # of significant_digits. If +false+, the # of fractional + # digits (defaults to +false+). + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). + # * <tt>:format</tt> - Specifies the format of the percentage + # string The number field is <tt>%n</tt> (defaults to "%n%"). + # + # ==== Examples + # + # number_to_percentage(100) # => 100.000% + # number_to_percentage("98") # => 98.000% + # number_to_percentage(100, :precision => 0) # => 100% + # number_to_percentage(1000, :delimiter => '.', :separator => ',') # => 1.000,000% + # number_to_percentage(302.24398923423, :precision => 5) # => 302.24399% + # number_to_percentage(1000, :locale => :fr) # => 1 000,000% + # number_to_percentage("98a") # => 98a% + # number_to_percentage(100, :format => "%n %") # => 100 % + # + def number_to_percentage(number, options = {}) + return unless number + options = options.symbolize_keys + + defaults = format_translations('percentage', options[:locale]) + options = defaults.merge!(options) + + format = options[:format] || "%n%" + + formatted_number = format.gsub('%n', self.number_to_rounded(number, options)) + formatted_number + end + + # Formats a +number+ with grouped thousands using +delimiter+ + # (e.g., 12,324). You can customize the format in the +options+ + # hash. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ","). + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # + # ==== Examples + # + # number_to_delimited(12345678) # => 12,345,678 + # number_to_delimited("123456") # => 123,456 + # number_to_delimited(12345678.05) # => 12,345,678.05 + # number_to_delimited(12345678, :delimiter => ".") # => 12.345.678 + # number_to_delimited(12345678, :delimiter => ",") # => 12,345,678 + # number_to_delimited(12345678.05, :separator => " ") # => 12,345,678 05 + # number_to_delimited(12345678.05, :locale => :fr) # => 12 345 678,05 + # number_to_delimited("112a") # => 112a + # number_to_delimited(98765432.98, :delimiter => " ", :separator => ",") + # # => 98 765 432,98 + def number_to_delimited(number, options = {}) + options = options.symbolize_keys + + return number unless valid_float?(number) + + options = defaults_translations(options[:locale]).merge(options) + + parts = number.to_s.to_str.split('.') + parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") + parts.join(options[:separator]) + end + + # Formats a +number+ with the specified level of + # <tt>:precision</tt> (e.g., 112.32 has a precision of 2 if + # +:significant+ is +false+, and 5 if +:significant+ is +true+). + # You can customize the format in the +options+ hash. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number + # (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # + # of significant_digits. If +false+, the # of fractional + # digits (defaults to +false+). + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). + # + # ==== Examples + # + # number_to_rounded(111.2345) # => 111.235 + # number_to_rounded(111.2345, :precision => 2) # => 111.23 + # number_to_rounded(13, :precision => 5) # => 13.00000 + # number_to_rounded(389.32314, :precision => 0) # => 389 + # number_to_rounded(111.2345, :significant => true) # => 111 + # number_to_rounded(111.2345, :precision => 1, :significant => true) # => 100 + # number_to_rounded(13, :precision => 5, :significant => true) # => 13.000 + # number_to_rounded(111.234, :locale => :fr) # => 111,234 + # + # number_to_rounded(13, :precision => 5, :significant => true, :strip_insignificant_zeros => true) + # # => 13 + # + # number_to_rounded(389.32314, :precision => 4, :significant => true) # => 389.3 + # number_to_rounded(1111.2345, :precision => 2, :separator => ',', :delimiter => '.') + # # => 1.111,23 + def number_to_rounded(number, options = {}) + options = options.symbolize_keys + + return number unless valid_float?(number) + number = Float(number) + + defaults = format_translations('precision', options[:locale]) + options = defaults.merge!(options) + + precision = options.delete :precision + significant = options.delete :significant + strip_insignificant_zeros = options.delete :strip_insignificant_zeros + + if significant and precision > 0 + if number == 0 + digits, rounded_number = 1, 0 + else + digits = (Math.log10(number.abs) + 1).floor + rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new((10 ** (digits - precision)).to_f.to_s)).round.to_f * 10 ** (digits - precision) + digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed + end + precision -= digits + precision = precision > 0 ? precision : 0 #don't let it be negative + else + rounded_number = BigDecimal.new(number.to_s).round(precision).to_f + rounded_number = rounded_number.zero? ? rounded_number.abs : rounded_number #prevent showing negative zeros + end + formatted_number = self.number_to_delimited("%01.#{precision}f" % rounded_number, options) + if strip_insignificant_zeros + escaped_separator = Regexp.escape(options[:separator]) + formatted_number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + else + formatted_number + end + end + + STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb].freeze + + # Formats the bytes in +number+ into a more understandable + # representation (e.g., giving it 1500 yields 1.5 KB). This + # method is useful for reporting file sizes to users. You can + # customize the format in the +options+ hash. + # + # See <tt>number_to_human</tt> if you want to pretty-print a + # generic number. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number + # (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # + # of significant_digits. If +false+, the # of fractional + # digits (defaults to +true+) + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +true+) + # * <tt>:prefix</tt> - If +:si+ formats the number using the SI + # prefix (defaults to :binary) + # + # ==== Examples + # + # number_to_human_size(123) # => 123 Bytes + # number_to_human_size(1234) # => 1.21 KB + # number_to_human_size(12345) # => 12.1 KB + # number_to_human_size(1234567) # => 1.18 MB + # number_to_human_size(1234567890) # => 1.15 GB + # number_to_human_size(1234567890123) # => 1.12 TB + # number_to_human_size(1234567, :precision => 2) # => 1.2 MB + # number_to_human_size(483989, :precision => 2) # => 470 KB + # number_to_human_size(1234567, :precision => 2, :separator => ',') # => 1,2 MB + # + # Non-significant zeros after the fractional separator are + # stripped out by default (set + # <tt>:strip_insignificant_zeros</tt> to +false+ to change that): + # number_to_human_size(1234567890123, :precision => 5) # => "1.1229 TB" + # number_to_human_size(524288000, :precision => 5) # => "500 MB" + def number_to_human_size(number, options = {}) + options = options.symbolize_keys + + return number unless valid_float?(number) + number = Float(number) + + defaults = format_translations('human', options[:locale]) + options = defaults.merge!(options) + + #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files + options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) + + storage_units_format = I18n.translate(:'number.human.storage_units.format', :locale => options[:locale], :raise => true) + + base = options[:prefix] == :si ? 1000 : 1024 + + if number.to_i < base + unit = I18n.translate(:'number.human.storage_units.units.byte', :locale => options[:locale], :count => number.to_i, :raise => true) + storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit) + else + max_exp = STORAGE_UNITS.size - 1 + exponent = (Math.log(number) / Math.log(base)).to_i # Convert to base + exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit + number /= base ** exponent + + unit_key = STORAGE_UNITS[exponent] + unit = I18n.translate(:"number.human.storage_units.units.#{unit_key}", :locale => options[:locale], :count => number, :raise => true) + + formatted_number = self.number_to_rounded(number, options) + storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit) + end + end + + DECIMAL_UNITS = {0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion, + -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto}.freeze + + # Pretty prints (formats and approximates) a number in a way it + # is more readable by humans (eg.: 1200000000 becomes "1.2 + # Billion"). This is useful for numbers that can get very large + # (and too hard to read). + # + # See <tt>number_to_human_size</tt> if you want to print a file + # size. + # + # You can also define you own unit-quantifier names if you want + # to use other decimal units (eg.: 1500 becomes "1.5 + # kilometers", 0.150 becomes "150 milliliters", etc). You may + # define a wide range of unit quantifiers, even fractional ones + # (centi, deci, mili, etc). + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number + # (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # + # of significant_digits. If +false+, the # of fractional + # digits (defaults to +true+) + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +true+) + # * <tt>:units</tt> - A Hash of unit quantifier names. Or a + # string containing an i18n scope where to find this hash. It + # might have the following keys: + # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>, + # *<tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, + # *<tt>:billion</tt>, <tt>:trillion</tt>, + # *<tt>:quadrillion</tt> + # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>, + # *<tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, + # *<tt>:pico</tt>, <tt>:femto</tt> + # * <tt>:format</tt> - Sets the format of the output string + # (defaults to "%n %u"). The field types are: + # * %u - The quantifier (ex.: 'thousand') + # * %n - The number + # + # ==== Examples + # + # number_to_human(123) # => "123" + # number_to_human(1234) # => "1.23 Thousand" + # number_to_human(12345) # => "12.3 Thousand" + # number_to_human(1234567) # => "1.23 Million" + # number_to_human(1234567890) # => "1.23 Billion" + # number_to_human(1234567890123) # => "1.23 Trillion" + # number_to_human(1234567890123456) # => "1.23 Quadrillion" + # number_to_human(1234567890123456789) # => "1230 Quadrillion" + # number_to_human(489939, :precision => 2) # => "490 Thousand" + # number_to_human(489939, :precision => 4) # => "489.9 Thousand" + # number_to_human(1234567, :precision => 4, + # :significant => false) # => "1.2346 Million" + # number_to_human(1234567, :precision => 1, + # :separator => ',', + # :significant => false) # => "1,2 Million" + # + # Non-significant zeros after the decimal separator are stripped + # out by default (set <tt>:strip_insignificant_zeros</tt> to + # +false+ to change that): + # number_to_human(12345012345, :significant_digits => 6) # => "12.345 Billion" + # number_to_human(500000000, :precision => 5) # => "500 Million" + # + # ==== Custom Unit Quantifiers + # + # You can also use your own custom unit quantifiers: + # number_to_human(500000, :units => {:unit => "ml", :thousand => "lt"}) # => "500 lt" + # + # If in your I18n locale you have: + # distance: + # centi: + # one: "centimeter" + # other: "centimeters" + # unit: + # one: "meter" + # other: "meters" + # thousand: + # one: "kilometer" + # other: "kilometers" + # billion: "gazillion-distance" + # + # Then you could do: + # + # number_to_human(543934, :units => :distance) # => "544 kilometers" + # number_to_human(54393498, :units => :distance) # => "54400 kilometers" + # number_to_human(54393498000, :units => :distance) # => "54.4 gazillion-distance" + # number_to_human(343, :units => :distance, :precision => 1) # => "300 meters" + # number_to_human(1, :units => :distance) # => "1 meter" + # number_to_human(0.34, :units => :distance) # => "34 centimeters" + def number_to_human(number, options = {}) + options = options.symbolize_keys + + return number unless valid_float?(number) + number = Float(number) + + defaults = format_translations('human', options[:locale]) + options = defaults.merge!(options) + + #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files + options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) + + inverted_du = DECIMAL_UNITS.invert + + units = options.delete :units + unit_exponents = case units + when Hash + units + when String, Symbol + I18n.translate(:"#{units}", :locale => options[:locale], :raise => true) + when nil + I18n.translate(:"number.human.decimal_units.units", :locale => options[:locale], :raise => true) + else + raise ArgumentError, ":units must be a Hash or String translation scope." + end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e} + + number_exponent = number != 0 ? Math.log10(number.abs).floor : 0 + display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0 + number /= 10 ** display_exponent + + unit = case units + when Hash + units[DECIMAL_UNITS[display_exponent]] + when String, Symbol + I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) + else + I18n.translate(:"number.human.decimal_units.units.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) + end + + decimal_format = options[:format] || I18n.translate(:'number.human.decimal_units.format', :locale => options[:locale], :default => "%n %u") + formatted_number = self.number_to_rounded(number, options) + decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip + end + + + def self.private_module_and_instance_method(method_name) + private method_name + private_class_method method_name + end + private_class_method :private_module_and_instance_method + + def format_translations(namespace, locale) + defaults_translations(locale).merge(translations_for(namespace, locale)) + end + private_module_and_instance_method :format_translations + + def defaults_translations(locale) + I18n.translate(:'number.format', :locale => locale, :default => {}) + end + private_module_and_instance_method :defaults_translations + + def translations_for(namespace, locale) + I18n.translate(:"number.#{namespace}.format", :locale => locale, :default => {}) + end + private_module_and_instance_method :translations_for + + def valid_float?(number) + Float(number) + rescue ArgumentError, TypeError + false + end + private_module_and_instance_method :valid_float? + + end +end
\ No newline at end of file diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb index e24a089650..a5987044b9 100644 --- a/activesupport/test/core_ext/bigdecimal_test.rb +++ b/activesupport/test/core_ext/bigdecimal_test.rb @@ -14,4 +14,9 @@ class BigDecimalTest < ActiveSupport::TestCase bd = BigDecimal.new '10' assert_equal bd, bd.to_d end + + def test_to_s + bd = BigDecimal.new '0.01' + assert_equal '0.01', bd.to_s + end end diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 1cb1e25d4c..435f4aa5a1 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -186,3 +186,264 @@ class NumericExtSizeTest < ActiveSupport::TestCase assert_equal 3458764513820540928, 3.exabyte end end + +class NumericExtFormattingTest < ActiveSupport::TestCase + def kilobytes(number) + number * 1024 + end + + def megabytes(number) + kilobytes(number) * 1024 + end + + def gigabytes(number) + megabytes(number) * 1024 + end + + def terabytes(number) + gigabytes(number) * 1024 + end + + def test_to_s__phone + assert_equal("555-1234", 5551234.to_s(:phone)) + assert_equal("800-555-1212", 8005551212.to_s(:phone)) + assert_equal("(800) 555-1212", 8005551212.to_s(:phone, :area_code => true)) + assert_equal("800 555 1212", 8005551212.to_s(:phone, :delimiter => " ")) + assert_equal("(800) 555-1212 x 123", 8005551212.to_s(:phone, :area_code => true, :extension => 123)) + assert_equal("800-555-1212", 8005551212.to_s(:phone, :extension => " ")) + assert_equal("555.1212", 5551212.to_s(:phone, :delimiter => '.')) + assert_equal("+1-800-555-1212", 8005551212.to_s(:phone, :country_code => 1)) + assert_equal("+18005551212", 8005551212.to_s(:phone, :country_code => 1, :delimiter => '')) + assert_equal("22-555-1212", 225551212.to_s(:phone)) + assert_equal("+45-22-555-1212", 225551212.to_s(:phone, :country_code => 45)) + end + + def test_to_s__currency + assert_equal("$1,234,567,890.50", 1234567890.50.to_s(:currency)) + assert_equal("$1,234,567,890.51", 1234567890.506.to_s(:currency)) + assert_equal("-$1,234,567,890.50", -1234567890.50.to_s(:currency)) + assert_equal("-$ 1,234,567,890.50", -1234567890.50.to_s(:currency, :format => "%u %n")) + assert_equal("($1,234,567,890.50)", -1234567890.50.to_s(:currency, :negative_format => "(%u%n)")) + assert_equal("$1,234,567,892", 1234567891.50.to_s(:currency, :precision => 0)) + assert_equal("$1,234,567,890.5", 1234567890.50.to_s(:currency, :precision => 1)) + assert_equal("£1234567890,50", 1234567890.50.to_s(:currency, :unit => "£", :separator => ",", :delimiter => "")) + end + + + def test_to_s__rounded + assert_equal("-111.235", -111.2346.to_s(:rounded)) + assert_equal("111.235", 111.2346.to_s(:rounded)) + assert_equal("31.83", 31.825.to_s(:rounded, :precision => 2)) + assert_equal("111.23", 111.2346.to_s(:rounded, :precision => 2)) + assert_equal("111.00", 111.to_s(:rounded, :precision => 2)) + assert_equal("3268", (32.6751 * 100.00).to_s(:rounded, :precision => 0)) + assert_equal("112", 111.50.to_s(:rounded, :precision => 0)) + assert_equal("1234567892", 1234567891.50.to_s(:rounded, :precision => 0)) + assert_equal("0", 0.to_s(:rounded, :precision => 0)) + assert_equal("0.00100", 0.001.to_s(:rounded, :precision => 5)) + assert_equal("0.001", 0.00111.to_s(:rounded, :precision => 3)) + assert_equal("10.00", 9.995.to_s(:rounded, :precision => 2)) + assert_equal("11.00", 10.995.to_s(:rounded, :precision => 2)) + assert_equal("0.00", -0.001.to_s(:rounded, :precision => 2)) + end + + def test_to_s__percentage + assert_equal("100.000%", 100.to_s(:percentage)) + assert_equal("100%", 100.to_s(:percentage, :precision => 0)) + assert_equal("302.06%", 302.0574.to_s(:percentage, :precision => 2)) + assert_equal("123.4%", 123.400.to_s(:percentage, :precision => 3, :strip_insignificant_zeros => true)) + assert_equal("1.000,000%", 1000.to_s(:percentage, :delimiter => '.', :separator => ',')) + assert_equal("1000.000 %", 1000.to_s(:percentage, :format => "%n %")) + end + + def test_to_s__delimited + assert_equal("12,345,678", 12345678.to_s(:delimited)) + assert_equal("0", 0.to_s(:delimited)) + assert_equal("123", 123.to_s(:delimited)) + assert_equal("123,456", 123456.to_s(:delimited)) + assert_equal("123,456.78", 123456.78.to_s(:delimited)) + assert_equal("123,456.789", 123456.789.to_s(:delimited)) + assert_equal("123,456.78901", 123456.78901.to_s(:delimited)) + assert_equal("123,456,789.78901", 123456789.78901.to_s(:delimited)) + assert_equal("0.78901", 0.78901.to_s(:delimited)) + end + + def test_to_s__delimited__with_options_hash + assert_equal '12 345 678', 12345678.to_s(:delimited, :delimiter => ' ') + assert_equal '12,345,678-05', 12345678.05.to_s(:delimited, :separator => '-') + assert_equal '12.345.678,05', 12345678.05.to_s(:delimited, :separator => ',', :delimiter => '.') + assert_equal '12.345.678,05', 12345678.05.to_s(:delimited, :delimiter => '.', :separator => ',') + end + + + def test_to_s__rounded_with_custom_delimiter_and_separator + assert_equal '31,83', 31.825.to_s(:rounded, :precision => 2, :separator => ',') + assert_equal '1.231,83', 1231.825.to_s(:rounded, :precision => 2, :separator => ',', :delimiter => '.') + end + + def test_to_s__rounded__with_significant_digits + assert_equal "124000", 123987.to_s(:rounded, :precision => 3, :significant => true) + assert_equal "120000000", 123987876.to_s(:rounded, :precision => 2, :significant => true ) + assert_equal "9775", 9775.to_s(:rounded, :precision => 4, :significant => true ) + assert_equal "5.4", 5.3923.to_s(:rounded, :precision => 2, :significant => true ) + assert_equal "5", 5.3923.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "1", 1.232.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "7", 7.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "1", 1.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "53", 52.7923.to_s(:rounded, :precision => 2, :significant => true ) + assert_equal "9775.00", 9775.to_s(:rounded, :precision => 6, :significant => true ) + assert_equal "5.392900", 5.3929.to_s(:rounded, :precision => 7, :significant => true ) + assert_equal "0.0", 0.to_s(:rounded, :precision => 2, :significant => true ) + assert_equal "0", 0.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "0.0001", 0.0001.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "0.000100", 0.0001.to_s(:rounded, :precision => 3, :significant => true ) + assert_equal "0.0001", 0.0001111.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "10.0", 9.995.to_s(:rounded, :precision => 3, :significant => true) + assert_equal "9.99", 9.994.to_s(:rounded, :precision => 3, :significant => true) + assert_equal "11.0", 10.995.to_s(:rounded, :precision => 3, :significant => true) + end + + def test_to_s__rounded__with_strip_insignificant_zeros + assert_equal "9775.43", 9775.43.to_s(:rounded, :precision => 4, :strip_insignificant_zeros => true ) + assert_equal "9775.2", 9775.2.to_s(:rounded, :precision => 6, :significant => true, :strip_insignificant_zeros => true ) + assert_equal "0", 0.to_s(:rounded, :precision => 6, :significant => true, :strip_insignificant_zeros => true ) + end + + def test_to_s__rounded__with_significant_true_and_zero_precision + # Zero precision with significant is a mistake (would always return zero), + # so we treat it as if significant was false (increases backwards compatibility for number_to_human_size) + assert_equal "124", 123.987.to_s(:rounded, :precision => 0, :significant => true) + assert_equal "12", 12.to_s(:rounded, :precision => 0, :significant => true ) + end + + def test_to_s__human_size + assert_equal '0 Bytes', 0.to_s(:human_size) + assert_equal '1 Byte', 1.to_s(:human_size) + assert_equal '3 Bytes', 3.14159265.to_s(:human_size) + assert_equal '123 Bytes', 123.0.to_s(:human_size) + assert_equal '123 Bytes', 123.to_s(:human_size) + assert_equal '1.21 KB', 1234.to_s(:human_size) + assert_equal '12.1 KB', 12345.to_s(:human_size) + assert_equal '1.18 MB', 1234567.to_s(:human_size) + assert_equal '1.15 GB', 1234567890.to_s(:human_size) + assert_equal '1.12 TB', 1234567890123.to_s(:human_size) + assert_equal '1030 TB', terabytes(1026).to_s(:human_size) + assert_equal '444 KB', kilobytes(444).to_s(:human_size) + assert_equal '1020 MB', megabytes(1023).to_s(:human_size) + assert_equal '3 TB', terabytes(3).to_s(:human_size) + assert_equal '1.2 MB', 1234567.to_s(:human_size, :precision => 2) + assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :precision => 4) + assert_equal '1 KB', kilobytes(1.0123).to_s(:human_size, :precision => 2) + assert_equal '1.01 KB', kilobytes(1.0100).to_s(:human_size, :precision => 4) + assert_equal '10 KB', kilobytes(10.000).to_s(:human_size, :precision => 4) + assert_equal '1 Byte', 1.1.to_s(:human_size) + assert_equal '10 Bytes', 10.to_s(:human_size) + end + + def test_to_s__human_size_with_si_prefix + assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :prefix => :si) + assert_equal '123 Bytes', 123.0.to_s(:human_size, :prefix => :si) + assert_equal '123 Bytes', 123.to_s(:human_size, :prefix => :si) + assert_equal '1.23 KB', 1234.to_s(:human_size, :prefix => :si) + assert_equal '12.3 KB', 12345.to_s(:human_size, :prefix => :si) + assert_equal '1.23 MB', 1234567.to_s(:human_size, :prefix => :si) + assert_equal '1.23 GB', 1234567890.to_s(:human_size, :prefix => :si) + assert_equal '1.23 TB', 1234567890123.to_s(:human_size, :prefix => :si) + end + + def test_to_s__human_size_with_options_hash + assert_equal '1.2 MB', 1234567.to_s(:human_size, :precision => 2) + assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :precision => 4) + assert_equal '1 KB', kilobytes(1.0123).to_s(:human_size, :precision => 2) + assert_equal '1.01 KB', kilobytes(1.0100).to_s(:human_size, :precision => 4) + assert_equal '10 KB', kilobytes(10.000).to_s(:human_size, :precision => 4) + assert_equal '1 TB', 1234567890123.to_s(:human_size, :precision => 1) + assert_equal '500 MB', 524288000.to_s(:human_size, :precision=>3) + assert_equal '10 MB', 9961472.to_s(:human_size, :precision=>0) + assert_equal '40 KB', 41010.to_s(:human_size, :precision => 1) + assert_equal '40 KB', 41100.to_s(:human_size, :precision => 2) + assert_equal '1.0 KB', kilobytes(1.0123).to_s(:human_size, :precision => 2, :strip_insignificant_zeros => false) + assert_equal '1.012 KB', kilobytes(1.0123).to_s(:human_size, :precision => 3, :significant => false) + assert_equal '1 KB', kilobytes(1.0123).to_s(:human_size, :precision => 0, :significant => true) #ignores significant it precision is 0 + end + + def test_to_s__human_size_with_custom_delimiter_and_separator + assert_equal '1,01 KB', kilobytes(1.0123).to_s(:human_size, :precision => 3, :separator => ',') + assert_equal '1,01 KB', kilobytes(1.0100).to_s(:human_size, :precision => 4, :separator => ',') + assert_equal '1.000,1 TB', terabytes(1000.1).to_s(:human_size, :precision => 5, :delimiter => '.', :separator => ',') + end + + def test_number_to_human + assert_equal '-123', -123.to_s(:human) + assert_equal '-0.5', -0.5.to_s(:human) + assert_equal '0', 0.to_s(:human) + assert_equal '0.5', 0.5.to_s(:human) + assert_equal '123', 123.to_s(:human) + assert_equal '1.23 Thousand', 1234.to_s(:human) + assert_equal '12.3 Thousand', 12345.to_s(:human) + assert_equal '1.23 Million', 1234567.to_s(:human) + assert_equal '1.23 Billion', 1234567890.to_s(:human) + assert_equal '1.23 Trillion', 1234567890123.to_s(:human) + assert_equal '1.23 Quadrillion', 1234567890123456.to_s(:human) + assert_equal '1230 Quadrillion', 1234567890123456789.to_s(:human) + assert_equal '490 Thousand', 489939.to_s(:human, :precision => 2) + assert_equal '489.9 Thousand', 489939.to_s(:human, :precision => 4) + assert_equal '489 Thousand', 489000.to_s(:human, :precision => 4) + assert_equal '489.0 Thousand', 489000.to_s(:human, :precision => 4, :strip_insignificant_zeros => false) + assert_equal '1.2346 Million', 1234567.to_s(:human, :precision => 4, :significant => false) + assert_equal '1,2 Million', 1234567.to_s(:human, :precision => 1, :significant => false, :separator => ',') + assert_equal '1 Million', 1234567.to_s(:human, :precision => 0, :significant => true, :separator => ',') #significant forced to false + end + + def test_number_to_human_with_custom_units + #Only integers + volume = {:unit => "ml", :thousand => "lt", :million => "m3"} + assert_equal '123 lt', 123456.to_s(:human, :units => volume) + assert_equal '12 ml', 12.to_s(:human, :units => volume) + assert_equal '1.23 m3', 1234567.to_s(:human, :units => volume) + + #Including fractionals + distance = {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"} + assert_equal '1.23 mm', 0.00123.to_s(:human, :units => distance) + assert_equal '1.23 cm', 0.0123.to_s(:human, :units => distance) + assert_equal '1.23 dm', 0.123.to_s(:human, :units => distance) + assert_equal '1.23 m', 1.23.to_s(:human, :units => distance) + assert_equal '1.23 dam', 12.3.to_s(:human, :units => distance) + assert_equal '1.23 hm', 123.to_s(:human, :units => distance) + assert_equal '1.23 km', 1230.to_s(:human, :units => distance) + assert_equal '1.23 km', 1230.to_s(:human, :units => distance) + assert_equal '1.23 km', 1230.to_s(:human, :units => distance) + assert_equal '12.3 km', 12300.to_s(:human, :units => distance) + + #The quantifiers don't need to be a continuous sequence + gangster = {:hundred => "hundred bucks", :million => "thousand quids"} + assert_equal '1 hundred bucks', 100.to_s(:human, :units => gangster) + assert_equal '25 hundred bucks', 2500.to_s(:human, :units => gangster) + assert_equal '25 thousand quids', 25000000.to_s(:human, :units => gangster) + assert_equal '12300 thousand quids', 12345000000.to_s(:human, :units => gangster) + + #Spaces are stripped from the resulting string + assert_equal '4', 4.to_s(:human, :units => {:unit => "", :ten => 'tens '}) + assert_equal '4.5 tens', 45.to_s(:human, :units => {:unit => "", :ten => ' tens '}) + end + + def test_number_to_human_with_custom_format + assert_equal '123 times Thousand', 123456.to_s(:human, :format => "%n times %u") + volume = {:unit => "ml", :thousand => "lt", :million => "m3"} + assert_equal '123.lt', 123456.to_s(:human, :units => volume, :format => "%n.%u") + end + + def test_to_s__injected_on_proper_types + assert_equal Fixnum, 1230.class + assert_equal '1.23 Thousand', 1230.to_s(:human) + + assert_equal Float, Float(1230).class + assert_equal '1.23 Thousand', Float(1230).to_s(:human) + + assert_equal Bignum, (100**10).class + assert_equal '100000 Quadrillion', (100**10).to_s(:human) + + assert_equal BigDecimal, BigDecimal("1000010").class + assert_equal '1 Million', BigDecimal("1000010").to_s(:human) + end +end diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb new file mode 100644 index 0000000000..8bcdea7ce6 --- /dev/null +++ b/activesupport/test/number_helper_test.rb @@ -0,0 +1,373 @@ +require 'abstract_unit' +require 'active_support/number_helper' + +module ActiveSupport + module NumberHelper + class NumberHelperTest < ActiveSupport::TestCase + + class TestClassWithInstanceNumberHelpers + include ActiveSupport::NumberHelper + end + + class TestClassWithClassNumberHelpers + extend ActiveSupport::NumberHelper + end + + def setup + @instance_with_helpers = TestClassWithInstanceNumberHelpers.new + end + + def kilobytes(number) + number * 1024 + end + + def megabytes(number) + kilobytes(number) * 1024 + end + + def gigabytes(number) + megabytes(number) * 1024 + end + + def terabytes(number) + gigabytes(number) * 1024 + end + + def test_number_to_phone + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal("555-1234", number_helper.number_to_phone(5551234)) + assert_equal("800-555-1212", number_helper.number_to_phone(8005551212)) + assert_equal("(800) 555-1212", number_helper.number_to_phone(8005551212, {:area_code => true})) + assert_equal("", number_helper.number_to_phone("", {:area_code => true})) + assert_equal("800 555 1212", number_helper.number_to_phone(8005551212, {:delimiter => " "})) + assert_equal("(800) 555-1212 x 123", number_helper.number_to_phone(8005551212, {:area_code => true, :extension => 123})) + assert_equal("800-555-1212", number_helper.number_to_phone(8005551212, :extension => " ")) + assert_equal("555.1212", number_helper.number_to_phone(5551212, :delimiter => '.')) + assert_equal("800-555-1212", number_helper.number_to_phone("8005551212")) + assert_equal("+1-800-555-1212", number_helper.number_to_phone(8005551212, :country_code => 1)) + assert_equal("+18005551212", number_helper.number_to_phone(8005551212, :country_code => 1, :delimiter => '')) + assert_equal("22-555-1212", number_helper.number_to_phone(225551212)) + assert_equal("+45-22-555-1212", number_helper.number_to_phone(225551212, :country_code => 45)) + end + end + + def test_number_to_currency + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal("$1,234,567,890.50", number_helper.number_to_currency(1234567890.50)) + assert_equal("$1,234,567,890.51", number_helper.number_to_currency(1234567890.506)) + assert_equal("-$1,234,567,890.50", number_helper.number_to_currency(-1234567890.50)) + assert_equal("-$ 1,234,567,890.50", number_helper.number_to_currency(-1234567890.50, {:format => "%u %n"})) + assert_equal("($1,234,567,890.50)", number_helper.number_to_currency(-1234567890.50, {:negative_format => "(%u%n)"})) + assert_equal("$1,234,567,892", number_helper.number_to_currency(1234567891.50, {:precision => 0})) + assert_equal("$1,234,567,890.5", number_helper.number_to_currency(1234567890.50, {:precision => 1})) + assert_equal("£1234567890,50", number_helper.number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""})) + assert_equal("$1,234,567,890.50", number_helper.number_to_currency("1234567890.50")) + assert_equal("1,234,567,890.50 Kč", number_helper.number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"})) + assert_equal("1,234,567,890.50 - Kč", number_helper.number_to_currency("-1234567890.50", {:unit => "Kč", :format => "%n %u", :negative_format => "%n - %u"})) + end + end + + def test_number_to_percentage + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal("100.000%", number_helper.number_to_percentage(100)) + assert_equal("100%", number_helper.number_to_percentage(100, {:precision => 0})) + assert_equal("302.06%", number_helper.number_to_percentage(302.0574, {:precision => 2})) + assert_equal("100.000%", number_helper.number_to_percentage("100")) + assert_equal("1000.000%", number_helper.number_to_percentage("1000")) + assert_equal("123.4%", number_helper.number_to_percentage(123.400, :precision => 3, :strip_insignificant_zeros => true)) + assert_equal("1.000,000%", number_helper.number_to_percentage(1000, :delimiter => '.', :separator => ',')) + assert_equal("1000.000 %", number_helper.number_to_percentage(1000, :format => "%n %")) + end + end + + def test_to_delimited + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal("12,345,678", number_helper.number_to_delimited(12345678)) + assert_equal("0", number_helper.number_to_delimited(0)) + assert_equal("123", number_helper.number_to_delimited(123)) + assert_equal("123,456", number_helper.number_to_delimited(123456)) + assert_equal("123,456.78", number_helper.number_to_delimited(123456.78)) + assert_equal("123,456.789", number_helper.number_to_delimited(123456.789)) + assert_equal("123,456.78901", number_helper.number_to_delimited(123456.78901)) + assert_equal("123,456,789.78901", number_helper.number_to_delimited(123456789.78901)) + assert_equal("0.78901", number_helper.number_to_delimited(0.78901)) + assert_equal("123,456.78", number_helper.number_to_delimited("123456.78")) + end + end + + def test_to_delimited_with_options_hash + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '12 345 678', number_helper.number_to_delimited(12345678, :delimiter => ' ') + assert_equal '12,345,678-05', number_helper.number_to_delimited(12345678.05, :separator => '-') + assert_equal '12.345.678,05', number_helper.number_to_delimited(12345678.05, :separator => ',', :delimiter => '.') + assert_equal '12.345.678,05', number_helper.number_to_delimited(12345678.05, :delimiter => '.', :separator => ',') + end + end + + def test_to_rounded + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal("-111.235", number_helper.number_to_rounded(-111.2346)) + assert_equal("111.235", number_helper.number_to_rounded(111.2346)) + assert_equal("31.83", number_helper.number_to_rounded(31.825, :precision => 2)) + assert_equal("111.23", number_helper.number_to_rounded(111.2346, :precision => 2)) + assert_equal("111.00", number_helper.number_to_rounded(111, :precision => 2)) + assert_equal("111.235", number_helper.number_to_rounded("111.2346")) + assert_equal("31.83", number_helper.number_to_rounded("31.825", :precision => 2)) + assert_equal("3268", number_helper.number_to_rounded((32.6751 * 100.00), :precision => 0)) + assert_equal("112", number_helper.number_to_rounded(111.50, :precision => 0)) + assert_equal("1234567892", number_helper.number_to_rounded(1234567891.50, :precision => 0)) + assert_equal("0", number_helper.number_to_rounded(0, :precision => 0)) + assert_equal("0.00100", number_helper.number_to_rounded(0.001, :precision => 5)) + assert_equal("0.001", number_helper.number_to_rounded(0.00111, :precision => 3)) + assert_equal("10.00", number_helper.number_to_rounded(9.995, :precision => 2)) + assert_equal("11.00", number_helper.number_to_rounded(10.995, :precision => 2)) + assert_equal("0.00", number_helper.number_to_rounded(-0.001, :precision => 2)) + end + end + + def test_to_rounded_with_custom_delimiter_and_separator + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '31,83', number_helper.number_to_rounded(31.825, :precision => 2, :separator => ',') + assert_equal '1.231,83', number_helper.number_to_rounded(1231.825, :precision => 2, :separator => ',', :delimiter => '.') + end + end + + def test_to_rounded_with_significant_digits + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal "124000", number_helper.number_to_rounded(123987, :precision => 3, :significant => true) + assert_equal "120000000", number_helper.number_to_rounded(123987876, :precision => 2, :significant => true ) + assert_equal "40000", number_helper.number_to_rounded("43523", :precision => 1, :significant => true ) + assert_equal "9775", number_helper.number_to_rounded(9775, :precision => 4, :significant => true ) + assert_equal "5.4", number_helper.number_to_rounded(5.3923, :precision => 2, :significant => true ) + assert_equal "5", number_helper.number_to_rounded(5.3923, :precision => 1, :significant => true ) + assert_equal "1", number_helper.number_to_rounded(1.232, :precision => 1, :significant => true ) + assert_equal "7", number_helper.number_to_rounded(7, :precision => 1, :significant => true ) + assert_equal "1", number_helper.number_to_rounded(1, :precision => 1, :significant => true ) + assert_equal "53", number_helper.number_to_rounded(52.7923, :precision => 2, :significant => true ) + assert_equal "9775.00", number_helper.number_to_rounded(9775, :precision => 6, :significant => true ) + assert_equal "5.392900", number_helper.number_to_rounded(5.3929, :precision => 7, :significant => true ) + assert_equal "0.0", number_helper.number_to_rounded(0, :precision => 2, :significant => true ) + assert_equal "0", number_helper.number_to_rounded(0, :precision => 1, :significant => true ) + assert_equal "0.0001", number_helper.number_to_rounded(0.0001, :precision => 1, :significant => true ) + assert_equal "0.000100", number_helper.number_to_rounded(0.0001, :precision => 3, :significant => true ) + assert_equal "0.0001", number_helper.number_to_rounded(0.0001111, :precision => 1, :significant => true ) + assert_equal "10.0", number_helper.number_to_rounded(9.995, :precision => 3, :significant => true) + assert_equal "9.99", number_helper.number_to_rounded(9.994, :precision => 3, :significant => true) + assert_equal "11.0", number_helper.number_to_rounded(10.995, :precision => 3, :significant => true) + end + end + + def test_to_rounded_with_strip_insignificant_zeros + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal "9775.43", number_helper.number_to_rounded(9775.43, :precision => 4, :strip_insignificant_zeros => true ) + assert_equal "9775.2", number_helper.number_to_rounded(9775.2, :precision => 6, :significant => true, :strip_insignificant_zeros => true ) + assert_equal "0", number_helper.number_to_rounded(0, :precision => 6, :significant => true, :strip_insignificant_zeros => true ) + end + end + + def test_to_rounded_with_significant_true_and_zero_precision + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + # Zero precision with significant is a mistake (would always return zero), + # so we treat it as if significant was false (increases backwards compatibility for number_to_human_size) + assert_equal "124", number_helper.number_to_rounded(123.987, :precision => 0, :significant => true) + assert_equal "12", number_helper.number_to_rounded(12, :precision => 0, :significant => true ) + assert_equal "12", number_helper.number_to_rounded("12.3", :precision => 0, :significant => true ) + end + end + + def test_number_number_to_human_size + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '0 Bytes', number_helper.number_to_human_size(0) + assert_equal '1 Byte', number_helper.number_to_human_size(1) + assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265) + assert_equal '123 Bytes', number_helper.number_to_human_size(123.0) + assert_equal '123 Bytes', number_helper.number_to_human_size(123) + assert_equal '1.21 KB', number_helper.number_to_human_size(1234) + assert_equal '12.1 KB', number_helper.number_to_human_size(12345) + assert_equal '1.18 MB', number_helper.number_to_human_size(1234567) + assert_equal '1.15 GB', number_helper.number_to_human_size(1234567890) + assert_equal '1.12 TB', number_helper.number_to_human_size(1234567890123) + assert_equal '1030 TB', number_helper.number_to_human_size(terabytes(1026)) + assert_equal '444 KB', number_helper.number_to_human_size(kilobytes(444)) + assert_equal '1020 MB', number_helper.number_to_human_size(megabytes(1023)) + assert_equal '3 TB', number_helper.number_to_human_size(terabytes(3)) + assert_equal '1.2 MB', number_helper.number_to_human_size(1234567, :precision => 2) + assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265, :precision => 4) + assert_equal '123 Bytes', number_helper.number_to_human_size('123') + assert_equal '1 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 2) + assert_equal '1.01 KB', number_helper.number_to_human_size(kilobytes(1.0100), :precision => 4) + assert_equal '10 KB', number_helper.number_to_human_size(kilobytes(10.000), :precision => 4) + assert_equal '1 Byte', number_helper.number_to_human_size(1.1) + assert_equal '10 Bytes', number_helper.number_to_human_size(10) + end + end + + def test_number_to_human_size_with_si_prefix + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265, :prefix => :si) + assert_equal '123 Bytes', number_helper.number_to_human_size(123.0, :prefix => :si) + assert_equal '123 Bytes', number_helper.number_to_human_size(123, :prefix => :si) + assert_equal '1.23 KB', number_helper.number_to_human_size(1234, :prefix => :si) + assert_equal '12.3 KB', number_helper.number_to_human_size(12345, :prefix => :si) + assert_equal '1.23 MB', number_helper.number_to_human_size(1234567, :prefix => :si) + assert_equal '1.23 GB', number_helper.number_to_human_size(1234567890, :prefix => :si) + assert_equal '1.23 TB', number_helper.number_to_human_size(1234567890123, :prefix => :si) + end + end + + def test_number_to_human_size_with_options_hash + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '1.2 MB', number_helper.number_to_human_size(1234567, :precision => 2) + assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265, :precision => 4) + assert_equal '1 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 2) + assert_equal '1.01 KB', number_helper.number_to_human_size(kilobytes(1.0100), :precision => 4) + assert_equal '10 KB', number_helper.number_to_human_size(kilobytes(10.000), :precision => 4) + assert_equal '1 TB', number_helper.number_to_human_size(1234567890123, :precision => 1) + assert_equal '500 MB', number_helper.number_to_human_size(524288000, :precision=>3) + assert_equal '10 MB', number_helper.number_to_human_size(9961472, :precision=>0) + assert_equal '40 KB', number_helper.number_to_human_size(41010, :precision => 1) + assert_equal '40 KB', number_helper.number_to_human_size(41100, :precision => 2) + assert_equal '1.0 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 2, :strip_insignificant_zeros => false) + assert_equal '1.012 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 3, :significant => false) + assert_equal '1 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 0, :significant => true) #ignores significant it precision is 0 + end + end + + def test_number_to_human_size_with_custom_delimiter_and_separator + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '1,01 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 3, :separator => ',') + assert_equal '1,01 KB', number_helper.number_to_human_size(kilobytes(1.0100), :precision => 4, :separator => ',') + assert_equal '1.000,1 TB', number_helper.number_to_human_size(terabytes(1000.1), :precision => 5, :delimiter => '.', :separator => ',') + end + end + + def test_number_to_human + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '-123', number_helper.number_to_human(-123) + assert_equal '-0.5', number_helper.number_to_human(-0.5) + assert_equal '0', number_helper.number_to_human(0) + assert_equal '0.5', number_helper.number_to_human(0.5) + assert_equal '123', number_helper.number_to_human(123) + assert_equal '1.23 Thousand', number_helper.number_to_human(1234) + assert_equal '12.3 Thousand', number_helper.number_to_human(12345) + assert_equal '1.23 Million', number_helper.number_to_human(1234567) + assert_equal '1.23 Billion', number_helper.number_to_human(1234567890) + assert_equal '1.23 Trillion', number_helper.number_to_human(1234567890123) + assert_equal '1.23 Quadrillion', number_helper.number_to_human(1234567890123456) + assert_equal '1230 Quadrillion', number_helper.number_to_human(1234567890123456789) + assert_equal '490 Thousand', number_helper.number_to_human(489939, :precision => 2) + assert_equal '489.9 Thousand', number_helper.number_to_human(489939, :precision => 4) + assert_equal '489 Thousand', number_helper.number_to_human(489000, :precision => 4) + assert_equal '489.0 Thousand', number_helper.number_to_human(489000, :precision => 4, :strip_insignificant_zeros => false) + assert_equal '1.2346 Million', number_helper.number_to_human(1234567, :precision => 4, :significant => false) + assert_equal '1,2 Million', number_helper.number_to_human(1234567, :precision => 1, :significant => false, :separator => ',') + assert_equal '1 Million', number_helper.number_to_human(1234567, :precision => 0, :significant => true, :separator => ',') #significant forced to false + end + end + + def test_number_to_human_with_custom_units + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + #Only integers + volume = {:unit => "ml", :thousand => "lt", :million => "m3"} + assert_equal '123 lt', number_helper.number_to_human(123456, :units => volume) + assert_equal '12 ml', number_helper.number_to_human(12, :units => volume) + assert_equal '1.23 m3', number_helper.number_to_human(1234567, :units => volume) + + #Including fractionals + distance = {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"} + assert_equal '1.23 mm', number_helper.number_to_human(0.00123, :units => distance) + assert_equal '1.23 cm', number_helper.number_to_human(0.0123, :units => distance) + assert_equal '1.23 dm', number_helper.number_to_human(0.123, :units => distance) + assert_equal '1.23 m', number_helper.number_to_human(1.23, :units => distance) + assert_equal '1.23 dam', number_helper.number_to_human(12.3, :units => distance) + assert_equal '1.23 hm', number_helper.number_to_human(123, :units => distance) + assert_equal '1.23 km', number_helper.number_to_human(1230, :units => distance) + assert_equal '1.23 km', number_helper.number_to_human(1230, :units => distance) + assert_equal '1.23 km', number_helper.number_to_human(1230, :units => distance) + assert_equal '12.3 km', number_helper.number_to_human(12300, :units => distance) + + #The quantifiers don't need to be a continuous sequence + gangster = {:hundred => "hundred bucks", :million => "thousand quids"} + assert_equal '1 hundred bucks', number_helper.number_to_human(100, :units => gangster) + assert_equal '25 hundred bucks', number_helper.number_to_human(2500, :units => gangster) + assert_equal '25 thousand quids', number_helper.number_to_human(25000000, :units => gangster) + assert_equal '12300 thousand quids', number_helper.number_to_human(12345000000, :units => gangster) + + #Spaces are stripped from the resulting string + assert_equal '4', number_helper.number_to_human(4, :units => {:unit => "", :ten => 'tens '}) + assert_equal '4.5 tens', number_helper.number_to_human(45, :units => {:unit => "", :ten => ' tens '}) + end + end + + def test_number_to_human_with_custom_format + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '123 times Thousand', number_helper.number_to_human(123456, :format => "%n times %u") + volume = {:unit => "ml", :thousand => "lt", :million => "m3"} + assert_equal '123.lt', number_helper.number_to_human(123456, :units => volume, :format => "%n.%u") + end + end + + def test_number_helpers_should_return_nil_when_given_nil + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_nil number_helper.number_to_phone(nil) + assert_nil number_helper.number_to_currency(nil) + assert_nil number_helper.number_to_percentage(nil) + assert_nil number_helper.number_to_delimited(nil) + assert_nil number_helper.number_to_rounded(nil) + assert_nil number_helper.number_to_human_size(nil) + assert_nil number_helper.number_to_human(nil) + end + end + + def test_number_helpers_do_not_mutate_options_hash + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + options = { 'raise' => true } + + number_helper.number_to_phone(1, options) + assert_equal({ 'raise' => true }, options) + + number_helper.number_to_currency(1, options) + assert_equal({ 'raise' => true }, options) + + number_helper.number_to_percentage(1, options) + assert_equal({ 'raise' => true }, options) + + number_helper.number_to_delimited(1, options) + assert_equal({ 'raise' => true }, options) + + number_helper.number_to_rounded(1, options) + assert_equal({ 'raise' => true }, options) + + number_helper.number_to_human_size(1, options) + assert_equal({ 'raise' => true }, options) + + number_helper.number_to_human(1, options) + assert_equal({ 'raise' => true }, options) + end + end + + def test_number_helpers_should_return_non_numeric_param_unchanged + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal("+1-x x 123", number_helper.number_to_phone("x", :country_code => 1, :extension => 123)) + assert_equal("x", number_helper.number_to_phone("x")) + assert_equal("$x.", number_helper.number_to_currency("x.")) + assert_equal("$x", number_helper.number_to_currency("x")) + assert_equal("x%", number_helper.number_to_percentage("x")) + assert_equal("x", number_helper.number_to_delimited("x")) + assert_equal("x.", number_helper.number_to_rounded("x.")) + assert_equal("x", number_helper.number_to_rounded("x")) + assert_equal "x", number_helper.number_to_human_size('x') + assert_equal "x", number_helper.number_to_human('x') + end + end + + def test_extending_or_including_number_helper_correctly_hides_private_methods + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert !number_helper.respond_to?(:valid_float?) + assert number_helper.respond_to?(:valid_float?, true) + end + end + + end + end +end diff --git a/guides/source/active_support_core_extensions.textile b/guides/source/active_support_core_extensions.textile index 587f65529e..2addc50d68 100644 --- a/guides/source/active_support_core_extensions.textile +++ b/guides/source/active_support_core_extensions.textile @@ -1840,6 +1840,76 @@ date and time arithmetic. NOTE: Defined in +active_support/core_ext/numeric/time.rb+. +h4. Formatting + +Enables the formatting of numbers in a variety of ways. + +Produce a string representation of a number as a telephone number: +<ruby> +5551234.to_s(:phone) # => 555-1234 +1235551234.to_s(:phone) # => 123-555-1234 +1235551234.to_s(:phone, :area_code => true) # => (123) 555-1234 +1235551234.to_s(:phone, :delimiter => " ") # => 123 555 1234 +1235551234.to_s(:phone, :area_code => true, :extension => 555) # => (123) 555-1234 x 555 +1235551234.to_s(:phone, :country_code => 1) # => +1-123-555-1234 +</ruby> + +Produce a string representation of a number as currency: +<ruby> +1234567890.50.to_s(:currency) # => $1,234,567,890.50 +1234567890.506.to_s(:currency) # => $1,234,567,890.51 +1234567890.506.to_s(:currency, :precision => 3) # => $1,234,567,890.506 +</ruby> + +Produce a string representation of a number as a percentage: +<ruby> +100.to_s(:percentage) # => 100.000% +100.to_s(:percentage, :precision => 0) # => 100% +1000.to_s(:percentage, :delimiter => '.', :separator => ',') # => 1.000,000% +302.24398923423.to_s(:percentage, :precision => 5) # => 302.24399% +</ruby> + +Produce a string representation of a number in delimited form: +<ruby> +12345678.to_s(:delimited) # => 12,345,678 +12345678.05.to_s(:delimited) # => 12,345,678.05 +12345678.to_s(:delimited, :delimiter => ".") # => 12.345.678 +12345678.to_s(:delimited, :delimiter => ",") # => 12,345,678 +12345678.05.to_s(:delimited, :separator => " ") # => 12,345,678 05 +</ruby> + +Produce a string representation of a number rounded to a precision: +<ruby> +111.2345.to_s(:rounded) # => 111.235 +111.2345.to_s(:rounded, :precision => 2) # => 111.23 +13.to_s(:rounded, :precision => 5) # => 13.00000 +389.32314.to_s(:rounded, :precision => 0) # => 389 +111.2345.to_s(:rounded, :significant => true) # => 111 +</ruby> + +Produce a string representation of a number as a human-readable number of bytes: +<ruby> +123.to_s(:human_size) # => 123 Bytes +1234.to_s(:human_size) # => 1.21 KB +12345.to_s(:human_size) # => 12.1 KB +1234567.to_s(:human_size) # => 1.18 MB +1234567890.to_s(:human_size) # => 1.15 GB +1234567890123.to_s(:human_size) # => 1.12 TB +</ruby> + +Produce a string representation of a number in human-readable words: +<ruby> +123.to_s(:human) # => "123" +1234.to_s(:human) # => "1.23 Thousand" +12345.to_s(:human) # => "12.3 Thousand" +1234567.to_s(:human) # => "1.23 Million" +1234567890.to_s(:human) # => "1.23 Billion" +1234567890123.to_s(:human) # => "1.23 Trillion" +1234567890123456.to_s(:human) # => "1.23 Quadrillion" +</ruby> + +NOTE: Defined in +active_support/core_ext/numeric/formatting.rb+. + h3. Extensions to +Integer+ h4. +multiple_of?+ |