/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Logan Byam
*/
/*global Intl: false */
/**
* @private
* @module nmodule/bajaScript/rc/baja/obj/numberUtil
*/
define([ 'bajaScript/sys' ], function (
baja) {
'use strict';
var ZEROS = '00000000000000000000';
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString
var supportsLocales = !!(typeof Intl === 'object' && Intl && typeof Intl.NumberFormat === 'function');
var numberCharactersCache = {};
/**
* Utilities for working with numbers.
*
* @private
* @exports bajaScript/baja/obj/numberUtil
*/
var exports = {};
////////////////////////////////////////////////////////////////
// Support functions
////////////////////////////////////////////////////////////////
function isNumber(num) {
return baja.hasType(num, 'baja:Number');
}
function addSign(num, str, forceSign) {
return (forceSign && num >= 0 ? '+' : '') + str;
}
function neededZeros(actual, needed) {
if (needed > 0) {
if (needed > ZEROS.length) {
needed = ZEROS.length;
}
return ZEROS.substr(0, needed.valueOf() - actual);
}
return '';
}
function clampPrecision(num) {
return Math.max(Math.min(num, 20), 0);
}
function clampRadix(radix) {
return radix < 2 || radix > 36 ? 10 : radix;
}
function localeNumberFormat(num, lang, precision) {
var str = String(num);
if (str.match(/[Ee]/)) {
return str.replace('e', 'E');
}
var obj = isNumber(precision) ? {
minimumFractionDigits: +precision,
maximumFractionDigits: +precision
} : {};
return num.valueOf().toLocaleString(lang, obj);
}
function localeFormat(num, lang, precision) {
if (!supportsLocales) {
return;
}
try {
return localeNumberFormat(num, lang, precision);
} catch (e) {
try {
return localeNumberFormat(num, lang, clampPrecision(precision));
} catch (e2) {
//user configured language not recognized by the browser
baja.error(e2);
}
}
}
/**
* @param {baja.Simple|Number} num - a `baja:Number` or JavaScript `Number`.
* @param {string} lang - IETF language code to use for i18n
* @param {Number} [precision] - The number of decimal places to show in
* the return string. Specifying '0' will also remove the decimal.
* @returns {String}
*
* @example
* `toPrecision(1.5, 3) === '1.500';`
*
* `toPrecision(1.274, 1) === '1.3';`
*/
function toPrecision(num, lang, precision) {
if (isNumber(precision)) {
var str,
chars = exports.getNumberCharacters(lang);
try {
str = num.valueOf().toFixed(+precision);
} catch (e) {
str = num.valueOf().toFixed(clampPrecision(precision));
}
return str.replace('.', chars.decimal);
} else {
return num.encodeToString();
}
}
function formatNumberNoUnits(num, cx) {
cx = cx || {};
var encoded = num.encodeToString(),
asNumber = num.valueOf(),
radix = cx.radix;
// return in the case of 'min', 'max', '+inf', '-inf', 'nan'
if (isNaN(parseFloat(encoded))) {
return encoded;
} else if (radix) {
return asNumber.toString(clampRadix(radix.valueOf()));
}
var forceSign = cx.forceSign,
precision = cx.precision,
showSeparators = cx.showSeparators,
trimTrailingZeros = cx.trimTrailingZeros,
zeroPad = cx.zeroPad,
lang = cx.languageTag || baja.getLanguage(),
hasPrecision = isNumber(precision),
hasZeroPad = isNumber(zeroPad),
str = toPrecision(num, lang, precision),
defaultDecimal = exports.getNumberCharacters(lang).decimal,
idx,
decimals,
digits;
if (!showSeparators && !hasZeroPad) {
if (trimTrailingZeros) {
str = doTrimTrailingZeros(str, defaultDecimal);
}
//nothing to touch before the decimal place
return addSign(asNumber, str, forceSign);
}
if (!hasZeroPad) {
//zero pad wins over separators
str = localeFormat(num, lang, precision) || (+str).toLocaleString();
}
idx = str.lastIndexOf(defaultDecimal);
digits = idx < 0 ? str.length : str.substr(0, idx).length;
decimals = idx >= 0 ? str.substr(idx + 1).length : 0;
if (hasPrecision && decimals < precision.valueOf()) {
str += decimals ? '' : defaultDecimal;
str += neededZeros(decimals, precision);
}
if (hasZeroPad) {
str = neededZeros(digits, zeroPad) + str;
}
if (trimTrailingZeros) {
str = doTrimTrailingZeros(str, defaultDecimal);
}
return addSign(asNumber, str, forceSign);
}
function doTrimTrailingZeros(str, decimal) {
return str.replace(new RegExp('\\' + decimal + '\\d+$'), function (match) {
match = match.replace(/0+$/, '');
return match === decimal ? '' : match;
});
}
////////////////////////////////////////////////////////////////
// Exports
////////////////////////////////////////////////////////////////
/**
* Calculate the grouping and decimal characters used in the given language.
* @param {string} lang
* @returns {{ grouping: string, decimal: string }}
*/
exports.getNumberCharacters = function (lang) {
var existing = numberCharactersCache[lang];
if (existing) {
return existing;
}
var num = 99999.9,
str = localeFormat(num, lang, 1) || num.toLocaleString(),
match = str.match(/99([^9]?)999([^9]?)9/);
return (numberCharactersCache[lang] = {
grouping: match[1] || ',',
decimal: match[2] || '.'
});
};
/**
* Default decimal character; the "." in "1.5".
* @returns {string}
*/
exports.getDefaultDecimal = function () {
return exports.getNumberCharacters(baja.getLanguage() || 'en').decimal;
};
/**
* Default separator character; the "," in "1,234".
* @returns {string}
*/
exports.getDefaultGrouping = function () {
return exports.getNumberCharacters(baja.getLanguage() || 'en').grouping;
};
/**
* Convert the number to a display string.
*
* This accounts for units and unit conversion.
*
* @param {baja.Simple|Number} num - a `baja:Number` or JavaScript `Number`.
*
* @param {baja.Facets|Object} [cx] - Used to specify formatting facets. The
* argument can also be an Object Literal.
*
* @param {Boolean} [cx.forceSign] - specifying 'true' will concatenate a '+'
* to the beginning of the number if positive.
*
* @param {Number} [cx.precision] - The number of decimal places to show in
* the return string. Specifying '0' will also remove the decimal.
*
* @param {Number} [cx.radix] - Specify the number base of the return string.
*
* @param {Boolean} [cx.showSeparators] - include separators.
*
* @param {baja.Unit} [cx.units] - the baja Unit to apply to the returned
* String.
*
* @param {Boolean} [cx.showUnits] - if false, don't show the units. Units may still
* be converted if this is set to false.
*
* @param {baja.Enum|Number|String} [cx.unitConversion] - the
* `baja:UnitConversion` enum, an ordinal, or tag.
*
* @param {Number} [cx.zeroPad] - add leading zeros to ensure at least this
* many digits before the decimal place.
*
* @returns {Promise.<String>}
*/
exports.formatNumber = function (num, cx) {
cx = cx || {};
var units = cx.units,
unitConversion = exports.getUnitConversion(cx),
showUnits = cx.showUnits;
return exports.getDisplayUnits(units, unitConversion)
.then(function (displayUnits) {
if (displayUnits) {
num = num.make(units.convertTo(displayUnits, num.valueOf()));
var display = formatNumberNoUnits(num, cx),
symbol = displayUnits.getSymbol(),
prefix = displayUnits.isPrefix();
if (showUnits !== false) {
return prefix ? symbol + ' ' + display : display + ' ' + symbol;
} else {
return display;
}
}
return formatNumberNoUnits(num, cx);
});
};
/**
* @param {object} [cx] a context object
* @returns {number} `unitConversion` specified in context, or the
* user-configured `baja.getUnitConversion()` if none is specified
* @since Niagara 4.8
*/
exports.getUnitConversion = function (cx) {
if (cx && 'unitConversion' in cx) {
return cx.unitConversion;
}
return baja.getUnitConversion();
};
/**
* Convert a baja `Integer` or `Long` to a `String`.
*
* This accounts for units and unit conversion.
*
* @param {baja.Simple|Number} num - a `baja:Number` or JavaScript `Number`.
*
* @param {baja.Facets|Object} [cx] - Used to specify formatting facets. The
* argument can also be an Object Literal.
*
* @param {Boolean} [cx.forceSign] - specifying 'true' will concatenate a '+'
* to the beginning of the number if positive.
*
* @param {Number} [cx.radix] - Specify the number base of the return string.
*
* @param {Boolean} [cx.showSeparators] - include separators.
*
* @param {baja.Unit} [cx.units] - the baja Unit to apply to the returned
* String.
*
* @param {baja.Enum|Number|String} [cx.unitConversion] - the
* `baja:UnitConversion` enum, an ordinal, or tag.
*
* @param {Number} [cx.zeroPad] - add leading zeros to ensure at least this
* many digits before the decimal place.
*
* @returns {Promise.<String>}
*/
exports.integralToString = function (num, cx) {
cx = Object.create(cx || {});
cx.precision = 0;
return exports.formatNumber(num, cx);
};
/**
* Convert a baja `Double` or `Float` to a `String`.
*
* This accounts for units and unit conversion.
*
* @param {baja.Simple|Number} num - a `baja:Number` or JavaScript `Number`.
*
* @param {baja.Facets|Object} [cx] - Used to specify formatting facets. The
* argument can also be an Object Literal.
*
* @param {Boolean} [cx.forceSign] - specifying 'true' will concatenate a '+'
* to the beginning of the number if positive.
*
* @param {Number} [cx.precision] - The number of decimal places to show in
* the return string. Specifying '0' will also remove the decimal. If a context
* is provided without precision, this value will default to 2. If no context
* is provided, there will be no precision applied.
*
* @param {Boolean} [cx.showSeparators] - include separators.
*
* @param {baja.Unit} [cx.units] - the baja Unit to apply to the returned
* String.
*
* @param {baja.Enum|Number|String} [cx.unitConversion] - the `baja:UnitConversion`
* enum, an ordinal, or tag.
*
* @param {Number} [cx.zeroPad] - add leading zeros to ensure at least this
* many digits before the decimal place.
*
* @returns {Promise.<String>}
*/
exports.floatingPointToString = function (num, cx) {
var defaults = { precision: 2 };
var cxWithDefaults;
if (cx instanceof baja.Facets) {
cxWithDefaults = Object.assign(defaults, cx.toObject());
} else {
cxWithDefaults = Object.assign(defaults, cx);
}
return exports.formatNumber(num, cxWithDefaults);
};
/**
* Convert the given number in the specified unit, applying the given unit
* conversion.
*
* @param {Number} num
*
* @param {baja.Unit} unit - the units the given number is considered to be in
*
* @param {baja.Enum|Number|String} unitConversion - the `baja:UnitConversion`
* enum, an ordinal, or tag.
*
* @returns {Promise.<Number>}
*
* @example
* <caption>I know I have 32 degrees Fahrenheit (English), but the user wants
* to see metric units. What number should I show them?</caption>
*
* exports.convertUnitTo(32, fahrenheit, 'metric')
* .then(function (celsius) {
* expect(celsius).toBeCloseTo(0); //remember JS rounding inaccuracy
* });
*/
exports.convertUnitTo = function (num, unit, unitConversion) {
return exports.getDisplayUnits(unit, unitConversion, true)
.then(function (displayUnits) {
return displayUnits ?
num.make(unit.convertTo(displayUnits, num.valueOf())) :
num;
});
};
/**
* Convert the given number in the specified unit, removing the given unit
* conversion.
*
* @param {Number} num
*
* @param {baja.Unit} unit - the units we want to calculate for the given
* number
*
* @param {baja.Enum|Number|String} unitConversion - the `baja:UnitConversion`
* enum, an ordinal, or tag.
*
* @returns {Promise.<Number>}
*
* @example
* <caption>The user has the UI configured to show Celsius (metric), and has
* typed 0. But I know the underlying point is configured for Fahrenheit. What
* "real" number should I write to the point?</caption>
*
* exports.convertUnitFrom(0, fahrenheit, 'metric')
* .then(function (fahrenheit) {
* expect(fahrenheit).toBeCloseTo(32); //remember JS rounding inaccuracy
* });
*/
exports.convertUnitFrom = function (num, unit, unitConversion) {
return exports.getDisplayUnits(unit, unitConversion, true)
.then(function (displayUnits) {
return displayUnits ?
num.make(displayUnits.convertTo(unit, num.valueOf())) :
num;
});
};
/**
* Get the display unit specified by the given unit/conversion combination.
*
* @param {baja.Unit} [units]
*
* @param {baja.FrozenEnum|String|Number} [unitConversion]
*
* @returns {Promise} promise to be resolved with the desired display unit,
* or `null` if no unit given.
*/
exports.getDisplayUnits = function (units, unitConversion) {
return baja.Unit.toDisplayUnits({ units: units, unitConversion: unitConversion });
};
/**
* Return num as String with zero padding prepended if applicable.
*
* @param {Number|String} num
*
* @param {Number} n - minimum length of return String, padding with zeros if
* necessary. No truncation will occur if length of num String is greater than
* n.
*
* @returns {String}
*/
exports.addZeroPad = function (num, n) {
var str = num.toString(10);
return neededZeros(str.length, n) + str;
};
return exports;
});