# frozen_string_literal: true
class ReeNumber::NumberToHuman
include Ree::FnDSL
fn :number_to_human do
link :round_helper, import: -> { ROUND_MODES }
link :number_to_rounded
link :t, from: :ree_i18n
link :number_to_delimited
link :slice, from: :ree_hash
link 'ree_number/functions/constants', -> { DECIMAL_UNITS & INVERTED_DECIMAL_UNITS }
end
DEFAULTS = {
units: "decimal_units",
locale: :en,
format: "%n %u",
precision: 3,
significant: true,
strip_insignificant_zeros: true,
separator: ".",
delimiter: "",
round_mode: :default
}.freeze
doc(<<~DOC)
Pretty prints (formats and approximates) a number in a way it
is more readable by humans (e.g.: 1200000000 becomes "1.2
Billion"). This is useful for numbers that can get very large
(and too hard to read).
See number_to_human_size if you want to print a file
size.
You can also define your own unit-quantifier names if you want
to use other decimal units (e.g.: 1500 becomes "1.5
kilometers", 0.150 becomes "150 milliliters", etc). You may
define a wide range of unit quantifiers, even fractional ones
(centi, deci, mili, etc).
==== Options
* :locale - Sets the locale to be used for formatting
(defaults to current locale).
* :precision - Sets the precision of the number
(defaults to 3).
* :significant - If +true+, precision will be the number
of significant_digits. If +false+, the number of fractional
digits (defaults to +true+)
* :round_mode - Determine how rounding is performed
(defaults to :default. See BigDecimal::mode)
* :separator - Sets the separator between the
fractional and integer digits (defaults to ".").
* :delimiter - Sets the thousands delimiter (defaults
to "").
* :strip_insignificant_zeros - If +true+ removes
insignificant zeros after the decimal separator (defaults to
+true+)
* :units - A Hash of unit quantifier names. Or a
string containing an i18n scope where to find this hash. It
might have the following keys:
* *integers*: :unit, :ten,
:hundred, :thousand, :million,
:billion, :trillion,
:quadrillion
* *fractionals*: :deci, :centi,
:mili, :micro, :nano,
:pico, :femto
* :format - Sets the format of the output string
(defaults to "%n %u"). The field types are:
* %u - The quantifier (ex.: 'thousand')
* %n - The number
==== Examples
number_to_human(123)
# => "123"
number_to_human(1234)
# => "1.23 Thousand"
number_to_human(12345)
# => "12.3 Thousand"
number_to_human(1234567)
# => "1.23 Million"
number_to_human(1234567890)
# => "1.23 Billion"
number_to_human(1234567890123)
# => "1.23 Trillion"
number_to_human(1234567890123456)
# => "1.23 Quadrillion"
number_to_human(1234567890123456789)
# => "1230 Quadrillion"
number_to_human(489939, precision: 2)
# => "490 Thousand"
number_to_human(489939, precision: 4)
# => "489.9 Thousand"
number_to_human(1234567, precision: 4, significant: false)
# => "1.2346 Million"
number_to_human(1234567, precision: 1, separator: ',', significant: false)
# => "1,2 Million"
number_to_human(500000000, precision: 5)
# => "500 Million"
number_to_human(12345012345, significant: false)
# => "12.345 Billion"
Non-significant zeros after the decimal separator are stripped
out by default (set :strip_insignificant_zeros to
+false+ to change that):
number_to_human(12.00001)
# => "12"
number_to_human(12.00001, strip_insignificant_zeros: false)
# => "12.0"
DOC
contract(
Or[Integer, Float, String],
Ksplat[
units?: String,
locale?: Symbol,
format?: String,
precision?: Integer,
significant?: Bool,
strip_insignificant_zeros?: Bool,
separator?: String,
delimiter?: String,
round_mode?: Or[*ROUND_MODES]
] => String
)
def call(number, **opts)
options = DEFAULTS.merge(opts)
number = round_helper(
number,
**slice(options, [:precision, :significant, :round_mode])
)
number = Float(number)
exponent = calculate_exponent(
number, options[:locale], options[:units]
)
number = number / (10**exponent)
rounded_number = number_to_rounded(
number,
**slice(
options,
[:precision, :significant, :strip_insignificant_zeros, :round_mode]
)
)
unit = determine_unit(
exponent, options[:units], options[:locale]
)
result_number = options[:format]
.gsub("%n", rounded_number)
.gsub("%u", unit)
.strip
number_to_delimited(
result_number,
**slice(options, [:separator, :delimiter])
)
end
private
def determine_unit(exponent, units, locale)
exp = DECIMAL_UNITS[exponent]
case units
when Hash
units[exp] || ""
when String, Symbol
t("human.#{units}.#{exp}", locale: locale, default_by_locale: :en)
else
t("human.decimal_units.#{exp}", count: number.to_i, default_by_locale: :en)
end
end
def calculate_exponent(number, locale, units)
exponent = number != 0 ? Math.log10(number.abs).floor : 0
unit_exponents(units, locale).find { |e| exponent >= e } || 0
end
def unit_exponents(units, locale)
case units
when Hash
units
when String, Symbol
t("human.#{units}", locale: locale, raise: true, default_by_locale: :en)
else
raise ArgumentError, ":units must be a Hash or String translation scope."
end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by(&:-@)
end
end