diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index e0151baa3603422f15988610fadb6416e43b58ed..53f08efae57e5e96429ff3e39ec0195095cc4a0d 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -1,115 +1,16 @@ -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 - - DEFAULTS = { - # Used in 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 number_to_currency - currency: { - format: { - format: "%u%n", - negative_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: { - delimiter: "", - format: "%n%" - } - }, - # Used in number_to_rounded - precision: { - format: { - delimiter: "" - } - }, + autoload :NumberToRoundedConverter, "active_support/number_helper/number_to_rounded" + autoload :NumberToDelimitedConverter, "active_support/number_helper/number_to_delimited" + autoload :NumberToHumanConverter, "active_support/number_helper/number_to_human" + autoload :NumberToHumanSizeConverter, "active_support/number_helper/number_to_human_size" + autoload :NumberToPhoneConverter, "active_support/number_helper/number_to_phone" + autoload :NumberToCurrencyConverter, "active_support/number_helper/number_to_currency" + autoload :NumberToPercentageConverter, "active_support/number_helper/number_to_percentage" - # Used in number_to_human_size and number_to_human - human: { - format: { - # These five are to override number.format and are optional - 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: "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" - } - } - } - } - - 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 } - INVERTED_DECIMAL_UNITS = DECIMAL_UNITS.invert - STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb] + extend self # Formats a +number+ into a US phone number (e.g., (555) # 123-9876). You can customize the format in the +options+ hash. @@ -137,27 +38,7 @@ module NumberHelper # 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 + NumberToPhoneConverter.new(number, options).execute end # Formats a +number+ into a currency string (e.g., $13.65). You @@ -199,25 +80,7 @@ def number_to_phone(number, options = {}) # 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 = i18n_format_options(options[:locale], :currency) - currency[:negative_format] ||= "-" + currency[:format] if currency[:format] - - defaults = default_format_options(:currency).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.phase != 0 - format = options.delete(:negative_format) - number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '') - end - - format.gsub('%n', self.number_to_rounded(number, options)).gsub('%u', unit) + NumberToCurrencyConverter.new(number, options).execute end # Formats a +number+ as a percentage string (e.g., 65%). You can @@ -253,14 +116,7 @@ def number_to_currency(number, options = {}) # 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_options(options[:locale], :percentage) - options = defaults.merge!(options) - - format = options[:format] || "%n%" - format.gsub('%n', self.number_to_rounded(number, options)) + NumberToPercentageConverter.new(number, options).execute end # Formats a +number+ with grouped thousands using +delimiter+ @@ -289,15 +145,7 @@ def number_to_percentage(number, options = {}) # 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 = format_options(options[:locale]).merge!(options) - - parts = number.to_s.split('.') - parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") - parts.join(options[:separator]) + NumberToDelimitedConverter.new(number, options).execute end # Formats a +number+ with the specified level of @@ -340,39 +188,7 @@ def number_to_delimited(number, options = {}) # number_to_rounded(1111.2345, precision: 2, separator: ',', delimiter: '.') # # => 1.111,23 def number_to_rounded(number, options = {}) - return number unless valid_float?(number) - number = Float(number) - options = options.symbolize_keys - - defaults = format_options(options[:locale], :precision) - options = defaults.merge!(options) - - precision = options.delete :precision - significant = options.delete :significant - strip_insignificant_zeros = options.delete :strip_insignificant_zeros - - if significant && precision > 0 - if number == 0 - digits, rounded_number = 1, 0 - else - digits = (Math.log10(number.abs) + 1).floor - multiplier = 10 ** (digits - precision) - rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new(multiplier.to_f.to_s)).round.to_f * multiplier - digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed - end - precision -= digits - precision = 0 if precision < 0 # don't let it be negative - else - rounded_number = BigDecimal.new(number.to_s).round(precision).to_f - rounded_number = rounded_number.abs if rounded_number.zero? # 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 + NumberToRoundedConverter.new(number, options).execute end # Formats the bytes in +number+ into a more understandable @@ -420,36 +236,7 @@ def number_to_rounded(number, options = {}) # 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_options(options[:locale], :human) - 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 = translate_number_value_with_default('human.storage_units.format', :locale => options[:locale], :raise => true) - - base = options[:prefix] == :si ? 1000 : 1024 - - if number.to_i < base - unit = translate_number_value_with_default('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 = translate_number_value_with_default("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 + NumberToHumanSizeConverter.new(number, options).execute end # Pretty prints (formats and approximates) a number in a way it @@ -550,86 +337,8 @@ def number_to_human_size(number, options = {}) # 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_options(options[:locale], :human) - 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) - - units = options.delete :units - unit_exponents = case units - when Hash - units - when String, Symbol - I18n.translate(:"#{units}", :locale => options[:locale], :raise => true) - when nil - translate_number_value_with_default("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_DECIMAL_UNITS[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 - translate_number_value_with_default("human.decimal_units.units.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) - end - - decimal_format = options[:format] || translate_number_value_with_default('human.decimal_units.format', :locale => options[:locale]) - formatted_number = self.number_to_rounded(number, options) - decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip + NumberToHumanConverter.new(number, options).execute end - def self.private_module_and_instance_method(method_name) #:nodoc: - private method_name - private_class_method method_name - end - private_class_method :private_module_and_instance_method - - def format_options(locale, namespace = nil) #:nodoc: - default_format_options(namespace).merge!(i18n_format_options(locale, namespace)) - end - private_module_and_instance_method :format_options - - def default_format_options(namespace = nil) #:nodoc: - options = DEFAULTS[:format].dup - options.merge!(DEFAULTS[namespace][:format]) if namespace - options - end - private_module_and_instance_method :default_format_options - - def i18n_format_options(locale, namespace = nil) #:nodoc: - options = I18n.translate(:'number.format', locale: locale, default: {}).dup - if namespace - options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {})) - end - options - end - private_module_and_instance_method :i18n_format_options - - def translate_number_value_with_default(key, i18n_options = {}) #:nodoc: - default = key.split('.').reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] } - - I18n.translate(key, { default: default, scope: :number }.merge!(i18n_options)) - end - private_module_and_instance_method :translate_number_value_with_default - - def valid_float?(number) #:nodoc: - Float(number) - rescue ArgumentError, TypeError - false - end - private_module_and_instance_method :valid_float? end end diff --git a/activesupport/lib/active_support/number_helper/number_converter.rb b/activesupport/lib/active_support/number_helper/number_converter.rb new file mode 100644 index 0000000000000000000000000000000000000000..39255d14a35b0dfd09553c8478b23c1c40862eff --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_converter.rb @@ -0,0 +1,175 @@ +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' +require 'active_support/core_ext/class/attribute' + +module ActiveSupport + module NumberHelper + class NumberConverter # :nodoc: + + # Default and i18n option namespace per class + class_attribute :namespace + + # Does the object need a number that is a valid float? + class_attribute :need_valid_float + + attr_reader :number, :opts + + DEFAULTS = { + # Used in 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 number_to_currency + currency: { + format: { + format: "%u%n", + negative_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: { + delimiter: "", + format: "%n%" + } + }, + + # Used in number_to_rounded + precision: { + format: { + delimiter: "" + } + }, + + # Used in number_to_human_size and number_to_human + human: { + format: { + # These five are to override number.format and are optional + 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: "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" + } + } + } + } + + + def initialize(number, opts = {}) + @number = number + @opts = opts.symbolize_keys + end + + def execute + return unless @number + return @number if need_valid_float? && !valid_float? + convert + end + + private + + def options + @options ||= format_options.merge(opts) + end + + def format_options #:nodoc: + default_format_options.merge!(i18n_format_options) + end + + def default_format_options #:nodoc: + options = DEFAULTS[:format].dup + options.merge!(DEFAULTS[namespace][:format]) if namespace + options + end + + def i18n_format_options #:nodoc: + locale = opts[:locale] + options = I18n.translate(:'number.format', locale: locale, default: {}).dup + if namespace + options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {})) + end + options + end + + def translate_number_value_with_default(key, i18n_options = {}) #:nodoc: + I18n.translate(key, { default: default_value(key), scope: :number }.merge!(i18n_options)) + end + + def translate_in_locale(key, i18n_options = {}) + translate_number_value_with_default(key, { locale: options[:locale] }.merge(i18n_options)) + end + + def default_value(key) + key.split('.').reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] } + end + + def valid_float? #:nodoc: + Float(@number) + rescue ArgumentError, TypeError + false + end + + end + end +end diff --git a/activesupport/lib/active_support/number_helper/number_to_currency.rb b/activesupport/lib/active_support/number_helper/number_to_currency.rb new file mode 100644 index 0000000000000000000000000000000000000000..402b0b56aa6e6e3efba306bbf3b350b3d7db0027 --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_currency.rb @@ -0,0 +1,51 @@ +require 'active_support/number_helper/number_converter' +require 'active_support/number_helper/number_to_rounded' + +module ActiveSupport + module NumberHelper + class NumberToCurrencyConverter < NumberConverter # :nodoc: + + self.namespace = :currency + + def convert + number = @number.to_s.strip + format = options[:format] + + if is_negative?(number) + format = options[:negative_format] + number = absolute_value(number) + end + + rounded_number = NumberToRoundedConverter.new(number, options).execute + format.gsub('%n', rounded_number).gsub('%u', options[:unit]) + end + + private + + def is_negative?(number) + number.to_f.phase != 0 + end + + def absolute_value(number) + number.respond_to?("abs") ? number.abs : number.sub(/\A-/, '') + end + + def options + @options ||= begin + defaults = default_format_options.merge(i18n_opts) + # Override negative format if format options is given + defaults[:negative_format] = "-#{opts[:format]}" if opts[:format] + defaults.merge(opts) + end + end + + def i18n_opts + # Set International negative format if not exists + i18n = i18n_format_options + i18n[:negative_format] ||= "-#{i18n[:format]}" if i18n[:format] + i18n + end + + end + end +end diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited.rb b/activesupport/lib/active_support/number_helper/number_to_delimited.rb new file mode 100644 index 0000000000000000000000000000000000000000..b543b0c19d441927965300bf83b52f290c673464 --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_delimited.rb @@ -0,0 +1,25 @@ +require 'active_support/number_helper/number_converter' + +module ActiveSupport + module NumberHelper + class NumberToDelimitedConverter < NumberConverter #:nodoc: + + self.need_valid_float = true + + DELIMITED_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/ + + def convert + parts.join(options[:separator]) + end + + private + + def parts + left, right = number.to_s.split('.') + left.gsub!(DELIMITED_REGEX) { "#{$1}#{options[:delimiter]}" } + [left, right].compact + end + + end + end +end diff --git a/activesupport/lib/active_support/number_helper/number_to_human.rb b/activesupport/lib/active_support/number_helper/number_to_human.rb new file mode 100644 index 0000000000000000000000000000000000000000..70d669f175494b669169c03754100f5ed90eeb92 --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_human.rb @@ -0,0 +1,71 @@ +require 'active_support/number_helper/number_converter' +require 'active_support/number_helper/number_to_rounded' + +module ActiveSupport + module NumberHelper + class NumberToHumanConverter < NumberConverter # :nodoc: + + 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 } + INVERTED_DECIMAL_UNITS = DECIMAL_UNITS.invert + + self.namespace = :human + self.need_valid_float = true + + def convert # :nodoc: + @number = Float(@number) + + # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files + unless options.key?(:strip_insignificant_zeros) + options[:strip_insignificant_zeros] = true + end + + units = opts[:units] + exponent = calculate_exponent(units) + @number = @number / (10 ** exponent) + + unit = determine_unit(units, exponent) + + rounded_number = NumberToRoundedConverter.new(@number, options).execute + format.gsub(/%n/, rounded_number).gsub(/%u/, unit).strip + end + + private + + def format + options[:format] || translate_in_locale('human.decimal_units.format') + end + + def determine_unit(units, exponent) + exp = DECIMAL_UNITS[exponent] + case units + when Hash + units[exp] || '' + when String, Symbol + I18n.translate("#{units}.#{exp}", :locale => options[:locale], :count => number.to_i) + else + translate_in_locale("human.decimal_units.units.#{exp}", count: number.to_i) + end + end + + def calculate_exponent(units) + exponent = number != 0 ? Math.log10(number.abs).floor : 0 + unit_exponents(units).find { |e| exponent >= e } || 0 + end + + def unit_exponents(units) + case units + when Hash + units + when String, Symbol + I18n.translate(units.to_s, :locale => options[:locale], :raise => true) + when nil + translate_in_locale("human.decimal_units.units", raise: true) + else + raise ArgumentError, ":units must be a Hash or String translation scope." + end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by { |e| -e } + end + + end + end +end diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size.rb b/activesupport/lib/active_support/number_helper/number_to_human_size.rb new file mode 100644 index 0000000000000000000000000000000000000000..c0930564bc268dd0c58c1110a6ef087b0210003e --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_human_size.rb @@ -0,0 +1,63 @@ +require 'active_support/number_helper/number_converter' +require 'active_support/number_helper/number_to_rounded' + +module ActiveSupport + module NumberHelper + class NumberToHumanSizeConverter < NumberConverter + + STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb] + + self.namespace = :human + self.need_valid_float = true + + def convert + @number = Float(@number) + + # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files + unless options.key?(:strip_insignificant_zeros) + options[:strip_insignificant_zeros] = true + end + + if smaller_than_base? + number_to_format = @number.to_i.to_s + else + human_size = @number / (base ** exponent) + number_to_format = NumberToRoundedConverter.new(human_size, options).execute + end + conversion_format.gsub(/%n/, number_to_format).gsub(/%u/, unit) + end + + private + + def conversion_format + translate_number_value_with_default('human.storage_units.format', :locale => options[:locale], :raise => true) + end + + def unit + translate_number_value_with_default(storage_unit_key, :locale => options[:locale], :count => @number.to_i, :raise => true) + end + + def storage_unit_key + key_end = smaller_than_base? ? 'byte' : STORAGE_UNITS[exponent] + "human.storage_units.units.#{key_end}" + end + + def exponent + max = STORAGE_UNITS.size - 1 + exp = (Math.log(@number) / Math.log(base)).to_i + exp = max if exp > max # avoid overflow for the highest unit + exp + end + + def smaller_than_base? + @number.to_i < base + end + + def base + opts[:prefix] == :si ? 1000 : 1024 + end + + end + end +end + diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage.rb b/activesupport/lib/active_support/number_helper/number_to_percentage.rb new file mode 100644 index 0000000000000000000000000000000000000000..25e136be603ed58575b3898d6456a182a78d3a54 --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_percentage.rb @@ -0,0 +1,17 @@ +require 'active_support/number_helper/number_converter' +require 'active_support/number_helper/number_to_rounded' + +module ActiveSupport + module NumberHelper + class NumberToPercentageConverter < NumberConverter # :nodoc: + + self.namespace = :percentage + + def convert + rounded_number = NumberToRoundedConverter.new(number, options).execute + options[:format].gsub('%n', rounded_number) + end + + end + end +end diff --git a/activesupport/lib/active_support/number_helper/number_to_phone.rb b/activesupport/lib/active_support/number_helper/number_to_phone.rb new file mode 100644 index 0000000000000000000000000000000000000000..171c2dff189e990495a9dabbc5af97cc7ab0df2b --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_phone.rb @@ -0,0 +1,54 @@ +require 'active_support/number_helper/number_converter' +require 'active_support/number_helper/number_to_rounded' + +module ActiveSupport + module NumberHelper + class NumberToPhoneConverter < NumberConverter + def convert + str = '' + str << country_code(opts[:country_code]) + str << convert_to_phone_number(@number.to_s.strip) + str << phone_ext(opts[:extension]) + end + + private + + def convert_to_phone_number(number) + if opts[:area_code] + convert_with_area_code(number) + else + convert_without_area_code(number) + end + end + + def convert_with_area_code(number) + number.gsub(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3") + end + + def convert_without_area_code(number) + number.tap { |n| + n.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") + n.slice!(0, 1) if begins_with_delimiter?(n) + } + end + + def begins_with_delimiter?(number) + number.start_with?(delimiter) && !delimiter.blank? + end + + def delimiter + opts[:delimiter] || "-" + end + + def country_code(code) + code.blank? ? "" : "+#{code}#{delimiter}" + end + + def phone_ext(ext) + ext.blank? ? "" : " x #{ext}" + end + + end + end +end + diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded.rb b/activesupport/lib/active_support/number_helper/number_to_rounded.rb new file mode 100644 index 0000000000000000000000000000000000000000..817ac1ebb23f40d909546131a51f31d97b72298d --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_rounded.rb @@ -0,0 +1,62 @@ +require 'active_support/number_helper/number_converter' + +module ActiveSupport + module NumberHelper + class NumberToRoundedConverter < NumberConverter # :nodoc: + + self.namespace = :precision + self.need_valid_float = true + + def convert + @number = Float(@number) + + precision = options.delete :precision + significant = options.delete :significant + + if significant && precision > 0 + digits, rounded_number = digits_and_rounded_number(precision) + precision -= digits + precision = 0 if precision < 0 # don't let it be negative + else + rounded_number = BigDecimal.new(@number.to_s).round(precision).to_f + rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros + end + delimited_number = NumberToDelimitedConverter.new("%01.#{precision}f" % rounded_number, options).execute + format_number(delimited_number) + end + + private + + def digits_and_rounded_number(precision) + return [1,0] if @number.zero? + digits = digit_count(@number) + multiplier = 10 ** (digits - precision) + rounded_number = calculate_rounded_number(multiplier) + digits = digit_count(rounded_number) # After rounding, the number of digits may have changed + [digits, rounded_number] + end + + def calculate_rounded_number(multiplier) + (BigDecimal.new(@number.to_s) / BigDecimal.new(multiplier.to_f.to_s)).round.to_f * multiplier + end + + def digit_count(number) + (Math.log10(number.abs) + 1).floor + end + + def strip_insignificant_zeros + options[:strip_insignificant_zeros] + end + + def format_number(number) + if strip_insignificant_zeros + escaped_separator = Regexp.escape(options[:separator]) + number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + else + number + end + end + + end + end +end diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 1fadef36372ae250ece8f88f31638590ab89642a..61ca556fcd45cb097ab7229913333b59e343e0ad 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -370,12 +370,6 @@ def test_number_helpers_should_return_non_numeric_param_unchanged 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