Extract ActiveSupport::NumberHelper methods to classes

Due to the overall complexity of each method individually as well as the
global shared private module methods, this pulls each helper into it's
own converter class inheriting from a generic `NumberBuilder` class.

* The `NumberBuilder` class contains the private methods needed for each helper
method an eliminates the need for special definition of specialized private
module methods.
* The `ActiveSupport::NumberHelper::DEFAULTS` constant has been moved
into the `NumberBuilder` class because the `NumberBuilder` is the only
class which needs access to it.
* For each of the builders, the `#convert` method is broken down to
smaller parts and extracted into private methods for clarity of purpose.
* Most of the mutation that once was necessary has now been eliminated.
* Several of the mathematical operations for percentage, delimited, and
rounded have been moved into private methods to ease readability and
clarity.
* Internationalization is still a bit crufty, and definitely could be
improved, but it is functional and a bit easier to follow.

The following helpers were extracted into their respective classes.

* `#number_to_percentage` -> `NumberToPercentageConverter`
* `#number_to_delimited` -> `NumberToDelimitedConverter`
* `#number_to_phone` -> `NumberToPhoneConverter`
* `#number_to_currency` -> `NumberToCurrencyConverter`
* `#number_to_rounded` -> `NumberToRoundedConverter`
* `#number_to_human_size` -> `NumberToHumanSizeConverter`
* `#number_to_human` -> `NumberToHumanConverter`
This commit is contained in:
Matt Bridges 2013-06-12 11:53:29 -05:00
parent e47b6dee85
commit 2da9d67c27
10 changed files with 534 additions and 315 deletions

@ -1,116 +1,17 @@
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
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"
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: ""
}
},
# 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 }
STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb]
# 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: '&pound;', separator: ',', delimiter: '', format: '%n %u')
# # => 1234567890,50 &pound;
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,88 +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)
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
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_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
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

@ -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

@ -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

@ -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

@ -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 }
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)
inverted_decimal_units = DECIMAL_UNITS.invert
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

@ -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

@ -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

@ -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

@ -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

@ -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