#= 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)