#= require ./base
@Ultimate.Helpers.Number =
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.
*
* ==== Options
*
* * 'area_code' - Adds parentheses around the area code.
* * 'delimiter' - Specifies the delimiter to use
* (defaults to "-").
* * 'extension' - Specifies an extension to add to the
* end of the generated number.
* * 'country_code' - Sets the country code for the phone
* number.
* ==== Examples
*
* number_to_phone(5551234) # => 555-1234
* number_to_phone("5551234") # => 555-1234
* number_to_phone(1235551234) # => 123-555-1234
* number_to_phone(1235551234, area_code: true) # => (123) 555-1234
* number_to_phone(1235551234, delimiter: ' ') # => 123 555 1234
* number_to_phone(1235551234, area_code: true, extension: 555) # => (123) 555-1234 x 555
* number_to_phone(1235551234, country_code: 1) # => +1-123-555-1234
* number_to_phone("123a456") # => 123a456
*
* number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: '.')
* # => +1.123.555.1234 x 1343
###
number_to_phone: (number, options = {}) ->
return "" unless number?
number = _.string.strip(String(number))
area_code = options['area_code']
delimiter = options['delimiter'] ? "-"
extension = options['extension']
country_code = options['country_code']
if area_code
number = number.replace(/(\d{1,3})(\d{3})(\d{4}$)/, "($1) $2#{delimiter}$3")
else
number = number.replace(/(\d{0,3})(\d{3})(\d{4})$/, "$1#{delimiter}$2#{delimiter}$3")
number = number.slice(1) if _.string.startsWith(number, delimiter) and not _.string.isBlank(delimiter)
buf = []
buf.push "+#{country_code}#{delimiter}" unless _.string.isBlank(country_code)
buf.push number
buf.push " x #{extension}" unless _.string.isBlank(extension)
buf.join('')
###**
* Formats a +number+ into a currency string (e.g., $13.65). You
* can customize the format in the +options+ hash.
*
* ==== Options
*
* * 'locale' - Sets the locale to be used for formatting
* (defaults to current locale).
* * 'precision' - Sets the level of precision (defaults
* to 2).
* * 'unit' - Sets the denomination of the currency
* (defaults to "$").
* * 'separator' - Sets the separator between the units
* (defaults to ".").
* * 'delimiter' - Sets the thousands delimiter (defaults
* to ",").
* * 'format' - Sets the format for non-negative numbers
* (defaults to "%u%n"). Fields are %u for the
* currency, and %n for the number.
* * 'negative_format' - Sets the format for negative
* numbers (defaults to prepending an hyphen to the formatted
* number given by 'format'). Accepts the same fields
* than 'format', except %n is here the
* absolute value of the number.
*
* ==== Examples
*
* number_to_currency(1234567890.50) # => $1,234,567,890.50
* number_to_currency(1234567890.506) # => $1,234,567,890.51
* number_to_currency(1234567890.506, precision: 3) # => $1,234,567,890.506
* number_to_currency(1234567890.506, locale: 'fr') # => 1 234 567 890,51 €
* number_to_currency('123a456') # => $123a456
*
* number_to_currency(-1234567890.50, negative_format: '(%u%n)')
* # => ($1,234,567,890.50)
* number_to_currency(1234567890.50, unit: '£', separator: ',', delimiter: '')
* # => £1234567890,50
* number_to_currency(1234567890.50, unit: '£', separator: ',', delimiter: '', format: '%n %u')
* # => 1234567890,50 £
###
number_to_currency: (number, options = {}) ->
return "" unless number?
# TODO replace `currency` with `defaults`
currency = @i18n_format_options(options['locale'], 'currency')
currency['negative_format'] ||= "-" + currency['format'] if currency['format']
defaults = _.extend(@default_format_options('currency'), currency)
defaults['negative_format'] = "-" + options['format'] if options['format']
options = _.extend(defaults, options)
unit = _.outcasts.delete(options, 'unit')
format = _.outcasts.delete(options, 'format')
if number < 0
format = _.outcasts.delete(options, 'negative_format')
number = Math.abs(number)
format.replace('%n', @number_to_rounded(number, options)).replace('%u', unit)
###**
* Formats a +number+ as a percentage string (e.g., 65%). You can
* customize the format in the +options+ hash.
*
* ==== 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 #
* of significant_digits. If +false+, the # of fractional
* digits (defaults to +false+).
* * '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
* +false+).
* * 'format' - Specifies the format of the percentage
* string The number field is %n (defaults to "%n%").
*
* ==== Examples
*
* number_to_percentage(100) # => 100.000%
* number_to_percentage('98') # => 98.000%
* number_to_percentage(100, precision: 0) # => 100%
* number_to_percentage(1000, delimiter: '.', separator: ,') # => 1.000,000%
* number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
* number_to_percentage(1000, 'locale' => 'fr') # => 1 000,000%
* number_to_percentage('98a') # => 98a%
* number_to_percentage(100, format: '%n %') # => 100 %
###
number_to_percentage: (number, options = {}) ->
return "" unless number?
defaults = @format_options(options['locale'], 'percentage')
options = _.extend(defaults, options)
format = options['format'] or "%n%"
format.replace('%n', @number_to_rounded(number, options))
###**
* Formats a +number+ with grouped thousands using +delimiter+
* (e.g., 12,324). You can customize the format in the +options+
* hash.
*
* ==== Options
*
* * 'locale' - Sets the locale to be used for formatting
* (defaults to current locale).
* * 'delimiter' - Sets the thousands delimiter (defaults
* to ",").
* * 'separator' - Sets the separator between the
* fractional and integer digits (defaults to ".").
*
* ==== Examples
*
* number_to_delimited(12345678) # => 12,345,678
* number_to_delimited('123456') # => 123,456
* number_to_delimited(12345678.05) # => 12,345,678.05
* number_to_delimited(12345678, delimiter: '.') # => 12.345.678
* number_to_delimited(12345678, delimiter: ',') # => 12,345,678
* number_to_delimited(12345678.05, separator: ' ') # => 12,345,678 05
* number_to_delimited(12345678.05, locale: 'fr') # => 12 345 678,05
* number_to_delimited('112a') # => 112a
* number_to_delimited(98765432.98, delimiter: ' ', separator: ',')
* # => 98 765 432,98
###
number_to_delimited: (number, options = {}) ->
return "" unless number?
return number unless @valid_float(number)
options = _.extend(@format_options(options['locale']), options)
parts = String(number).split('.')
parts[0] = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1#{options['delimiter']}")
parts.join(options['separator'])
round_with_precision: (number, precision = 2) ->
precision = Math.pow(10, precision)
Math.round(number * precision) / precision
###**
* Formats a +number+ with the specified level of
* 'precision' (e.g., 112.32 has a precision of 2 if
* +'significant'+ is +false+, and 5 if +'significant'+ is +true+).
* You can customize the format in the +options+ hash.
*
* ==== 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 #
* of significant_digits. If +false+, the # of fractional
* digits (defaults to +false+).
* * '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
* +false+).
*
* ==== Examples
*
* number_to_rounded(111.2345) # => 111.235
* number_to_rounded(111.2345, precision: 2) # => 111.23
* number_to_rounded(13, precision: 5) # => 13.00000
* number_to_rounded(389.32314, precision: 0) # => 389
* number_to_rounded(111.2345, significant: true) # => 111
* number_to_rounded(111.2345, precision: 1, significant: true) # => 100
* number_to_rounded(13, precision: 5, significant: true) # => 13.000
* number_to_rounded(111.234, locale: 'fr') # => 111,234
*
* number_to_rounded(13, precision: 5, significant: true, strip_insignificant_zeros: true)
* # => 13
*
* number_to_rounded(389.32314, precision: 4, significant: true) # => 389.3
* number_to_rounded(1111.2345, precision: 2, separator: ',', delimiter: '.')
* # => 1.111,23
###
number_to_rounded: (number, options = {}) ->
return "" unless number?
return number unless @valid_float(number)
number = Number(number)
options = _.extend(@format_options(options['locale'], 'precision'), options)
precision = _.outcasts.delete(options, 'precision')
significant = _.outcasts.delete(options, 'significant')
strip_insignificant_zeros = _.outcasts.delete(options, 'strip_insignificant_zeros')
if significant and precision > 0
if number == 0
[digits, rounded_number] = [1, 0]
else
digits = Math.floor(Math.log(Math.abs(number)) / Math.LN10 + 1)
rounded_number = @round_with_precision(number, precision - digits)
digits = Math.floor(Math.log(Math.abs(rounded_number)) / Math.LN10 + 1) # After rounding, the number of digits may have changed
precision -= digits
precision = 0 if precision < 0 # don't let it be negative
else
rounded_number = @round_with_precision(number, precision)
formatted_number = @number_to_delimited(_.string.sprintf("%01.#{precision}f", rounded_number), options)
if strip_insignificant_zeros
escaped_separator = _.string.escapeRegExp(options['separator'])
formatted_number
.replace(new RegExp("(#{escaped_separator})(\\d*[1-9])?0+$"), '$1$2')
.replace(new RegExp("#{escaped_separator}$"), '')
else
formatted_number
###**
* Formats the bytes in +number+ into a more understandable
* representation (e.g., giving it 1500 yields 1.5 KB). This
* method is useful for reporting file sizes to users. You can
* customize the format in the +options+ hash.
*
* See number_to_human if you want to pretty-print a
* generic number.
*
* ==== 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 #
* of significant_digits. If +false+, the # of fractional
* digits (defaults to +true+)
* * '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+)
* * 'prefix' - If +'si'+ formats the number using the SI
* prefix (defaults to 'binary')
*
* ==== Examples
*
* number_to_human_size(123) # => 123 Bytes
* number_to_human_size(1234) # => 1.21 KB
* number_to_human_size(12345) # => 12.1 KB
* number_to_human_size(1234567) # => 1.18 MB
* number_to_human_size(1234567890) # => 1.15 GB
* number_to_human_size(1234567890123) # => 1.12 TB
* number_to_human_size(1234567, precision: 2) # => 1.2 MB
* number_to_human_size(483989, precision: 2) # => 470 KB
* number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB
*
* Non-significant zeros after the fractional separator are stripped out by
* default (set 'strip_insignificant_zeros' to +false+ to change that):
*
* number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB"
* number_to_human_size(524288000, precision: 5) # => "500 MB"
###
number_to_human_size: (number, options = {}) ->
return "" unless number?
return number unless @valid_float(number)
number = Number(number)
defaults = @format_options(options['locale'], 'human')
options = _.extend(defaults, options)
#for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
options['strip_insignificant_zeros'] = true if not _.has(options, 'strip_insignificant_zeros')
storage_units_format = @translate_number_value_with_default('human.storage_units.format', locale: options['locale'], raise: true)
base = if options['prefix'] is 'si' then 1000 else 1024
if parseInt(number) < base
unit = @translate_number_value_with_default('human.storage_units.units.byte', locale: options['locale'], count: parseInt(number), raise: true)
storage_units_format.replace(/%n/, parseInt(number)).replace(/%u/, unit)
else
max_exp = @STORAGE_UNITS.length - 1
exponent = parseInt(Math.log(number) / Math.log(base)) # Convert to base
exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
number /= Math.pow(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 = @number_to_rounded(number, options)
storage_units_format.replace(/%n/, formatted_number).replace(/%u/, unit)
###**
* Pretty prints (formats and approximates) a number in a way it
* is more readable by humans (eg.: 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 you own unit-quantifier names if you want
* to use other decimal units (eg.: 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 #
* of significant_digits. If +false+, the # of fractional
* digits (defaults to +true+)
* * '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"
*
* Non-significant zeros after the decimal separator are stripped
* out by default (set 'strip_insignificant_zeros' to
* +false+ to change that):
*
* number_to_human(12345012345, significant_digits: 6) # => "12.345 Billion"
* number_to_human(500000000, precision: 5) # => "500 Million"
*
* ==== Custom Unit Quantifiers
*
* You can also use your own custom unit quantifiers:
* number_to_human(500000, 'units' => {'unit' => "ml", 'thousand' => "lt"}) # => "500 lt"
*
* If in your I18n locale you have:
*
* distance:
* centi:
* one: "centimeter"
* other: "centimeters"
* unit:
* one: "meter"
* other: "meters"
* thousand:
* one: "kilometer"
* other: "kilometers"
* billion: "gazillion-distance"
*
* Then you could do:
*
* number_to_human(543934, 'units' => 'distance') # => "544 kilometers"
* number_to_human(54393498, 'units' => 'distance') # => "54400 kilometers"
* number_to_human(54393498000, 'units' => 'distance') # => "54.4 gazillion-distance"
* number_to_human(343, 'units' => 'distance', 'precision' => 1) # => "300 meters"
* number_to_human(1, 'units' => 'distance') # => "1 meter"
* number_to_human(0.34, 'units' => 'distance') # => "34 centimeters"
###
number_to_human: (number, options = {}) ->
return "" unless number?
return number unless @valid_float(number)
number = Number(number)
defaults = @format_options(options['locale'], 'human')
options = _.extend(defaults, options)
#for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
options['strip_insignificant_zeros'] = true unless _.has(options, 'strip_insignificant_zeros')
inverted_du = _.outcasts.invert(@DECIMAL_UNITS)
units = _.outcasts.delete options, 'units'
unit_exponents = if _.isObject(units)
units
else if _.isString(units)
I18n.translate(units, locale: options['locale'], raise: true)
else unless units?
@translate_number_value_with_default("human.decimal_units.units", locale: options['locale'], raise: true)
else
throw new Error "'units' must be a Hash or String translation scope."
unit_exponents = _.map(_.keys(unit_exponents), (e_name) -> parseInt(inverted_du[e_name])).sort((a, b) -> a < b)
number_exponent = if number isnt 0 then Math.floor(Math.log(Math.abs(number)) / Math.LN10) else 0
display_exponent = _.find(unit_exponents, (e) -> number_exponent >= e) or 0
number /= Math.pow(10, display_exponent)
unit = if _.isObject(units)
units[@DECIMAL_UNITS[display_exponent]]
else if _.isString(units)
I18n.translate("#{units}.#{@DECIMAL_UNITS[display_exponent]}", locale: options['locale'], count: parseInt(number))
else
@translate_number_value_with_default("human.decimal_units.units.#{@DECIMAL_UNITS[display_exponent]}", locale: options['locale'], count: parseInt(number))
decimal_format = options['format'] or @translate_number_value_with_default('human.decimal_units.format', locale: options['locale'])
formatted_number = @number_to_rounded(number, options)
_.string.strip decimal_format.replace(/%n/, formatted_number).replace(/%u/, unit)
#################### Private ####################
#:nodoc:
format_options: (locale, namespace = null) ->
_.extend(@default_format_options(namespace), @i18n_format_options(locale, namespace))
#:nodoc:
default_format_options: (namespace = null) ->
options = _.clone(@DEFAULTS['format'])
_.extend(options, @DEFAULTS[namespace]['format']) if namespace?
options
#:nodoc:
i18n_format_options: (locale, namespace = null) ->
options = _.clone(I18n.translate('number.format', locale: locale, default: {}))
if namespace?
_.extend(options, I18n.translate("number.#{namespace}.format", locale: locale, default: {}))
options
#:nodoc:
translate_number_value_with_default: (key, i18n_options = {}) ->
_default = _.reduce(key.split('.'), ((defaults, k) -> defaults[k]), @DEFAULTS)
I18n.translate(key, _.extend({default: _default, scope: 'number'}, i18n_options))
#:nodoc:
valid_float: (number) ->
not isNaN(number)