actionview/lib/action_view/helpers/number_helper.rb
# frozen_string_literal: true
require "active_support/core_ext/hash/keys"
require "active_support/core_ext/string/output_safety"
require "active_support/number_helper"
module ActionView
module Helpers # :nodoc:
# = Action View Number \Helpers
#
# Provides methods for converting numbers into formatted strings.
# Methods are provided for phone numbers, currency, percentage,
# precision, positional notation, file size, and pretty printing.
#
# Most methods expect a +number+ argument, and will return it
# unchanged if can't be converted into a valid number.
module NumberHelper
# Raised when argument +number+ param given to the helpers is invalid and
# the option +:raise+ is set to +true+.
class InvalidNumberError < StandardError
attr_accessor :number
def initialize(number)
@number = number
end
end
# Delegates to ActiveSupport::NumberHelper#number_to_phone.
#
# Additionally, supports a +:raise+ option that will cause
# InvalidNumberError to be raised if +number+ is not a valid number:
#
# number_to_phone("12x34") # => "12x34"
# number_to_phone("12x34", raise: true) # => InvalidNumberError
#
def number_to_phone(number, options = {})
return unless number
options = options.symbolize_keys
parse_float(number, true) if options.delete(:raise)
ERB::Util.html_escape(ActiveSupport::NumberHelper.number_to_phone(number, options))
end
# Delegates to ActiveSupport::NumberHelper#number_to_currency.
#
# Additionally, supports a +:raise+ option that will cause
# InvalidNumberError to be raised if +number+ is not a valid number:
#
# number_to_currency("12x34") # => "$12x34"
# number_to_currency("12x34", raise: true) # => InvalidNumberError
#
def number_to_currency(number, options = {})
delegate_number_helper_method(:number_to_currency, number, options)
end
# Delegates to ActiveSupport::NumberHelper#number_to_percentage.
#
# Additionally, supports a +:raise+ option that will cause
# InvalidNumberError to be raised if +number+ is not a valid number:
#
# number_to_percentage("99x") # => "99x%"
# number_to_percentage("99x", raise: true) # => InvalidNumberError
#
def number_to_percentage(number, options = {})
delegate_number_helper_method(:number_to_percentage, number, options)
end
# Delegates to ActiveSupport::NumberHelper#number_to_delimited.
#
# Additionally, supports a +:raise+ option that will cause
# InvalidNumberError to be raised if +number+ is not a valid number:
#
# number_with_delimiter("12x34") # => "12x34"
# number_with_delimiter("12x34", raise: true) # => InvalidNumberError
#
def number_with_delimiter(number, options = {})
delegate_number_helper_method(:number_to_delimited, number, options)
end
# Delegates to ActiveSupport::NumberHelper#number_to_rounded.
#
# Additionally, supports a +:raise+ option that will cause
# InvalidNumberError to be raised if +number+ is not a valid number:
#
# number_with_precision("12x34") # => "12x34"
# number_with_precision("12x34", raise: true) # => InvalidNumberError
#
def number_with_precision(number, options = {})
delegate_number_helper_method(:number_to_rounded, number, options)
end
# Delegates to ActiveSupport::NumberHelper#number_to_human_size.
#
# Additionally, supports a +:raise+ option that will cause
# InvalidNumberError to be raised if +number+ is not a valid number:
#
# number_to_human_size("12x34") # => "12x34"
# number_to_human_size("12x34", raise: true) # => InvalidNumberError
#
def number_to_human_size(number, options = {})
delegate_number_helper_method(:number_to_human_size, number, options)
end
# Delegates to ActiveSupport::NumberHelper#number_to_human.
#
# Additionally, supports a +:raise+ option that will cause
# InvalidNumberError to be raised if +number+ is not a valid number:
#
# number_to_human("12x34") # => "12x34"
# number_to_human("12x34", raise: true) # => InvalidNumberError
#
def number_to_human(number, options = {})
delegate_number_helper_method(:number_to_human, number, options)
end
private
def delegate_number_helper_method(method, number, options)
return unless number
options = escape_unsafe_options(options.symbolize_keys)
wrap_with_output_safety_handling(number, options.delete(:raise)) {
ActiveSupport::NumberHelper.public_send(method, number, options)
}
end
def escape_unsafe_options(options)
options[:format] = ERB::Util.html_escape(options[:format]) if options[:format]
options[:negative_format] = ERB::Util.html_escape(options[:negative_format]) if options[:negative_format]
options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator]
options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter]
options[:unit] = ERB::Util.html_escape(options[:unit]) if options[:unit] && !options[:unit].html_safe?
options[:units] = escape_units(options[:units]) if options[:units] && Hash === options[:units]
options
end
def escape_units(units)
units.transform_values do |v|
ERB::Util.html_escape(v)
end
end
def wrap_with_output_safety_handling(number, raise_on_invalid, &block)
valid_float = valid_float?(number)
raise InvalidNumberError, number if raise_on_invalid && !valid_float
formatted_number = yield
if valid_float || number.html_safe?
formatted_number.html_safe
else
formatted_number
end
end
def valid_float?(number)
!parse_float(number, false).nil?
end
def parse_float(number, raise_error)
result = Float(number, exception: false)
raise InvalidNumberError, number if result.nil? && raise_error
result
end
end
end
end