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:
parent
e47b6dee85
commit
2da9d67c27
@ -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: '£', 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,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
|
||||
|
Loading…
Reference in New Issue
Block a user