# frozen_string_literal: true
require "bigdecimal"
require "bigdecimal/util"
require "active_support/core_ext/big_decimal/conversions"
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 :validate_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 (e.g.: 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 self.convert(number, options)
new(number, options).execute
end
def initialize(number, options)
@number = number
@opts = options.symbolize_keys
@options = nil
end
def execute
if !number
nil
elsif validate_float? && !valid_bigdecimal
number
else
convert
end
end
private
def options
@options ||= format_options.merge(opts)
end
def format_options
default_format_options.merge!(i18n_format_options)
end
def default_format_options
options = DEFAULTS[:format].dup
options.merge!(DEFAULTS[namespace][:format]) if namespace
options
end
def i18n_format_options
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)
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_bigdecimal
case number
when Float, Rational
number.to_d(0)
when String
BigDecimal(number, exception: false)
else
number.to_d rescue nil
end
end
end
end
end