/*
* NumFmt.js - Number formatter definition
*
* Copyright © 2012-2015, 2018 JEDLSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
!depends
ilib.js
Locale.js
LocaleInfo.js
MathUtils.js
Currency.js
IString.js
JSUtils.js
*/
// !data localeinfo currency
var ilib = require("../index.js");
var JSUtils = require("./JSUtils.js");
var MathUtils = require("./MathUtils.js");
var Locale = require("./Locale.js");
var LocaleInfo = require("./LocaleInfo.js");
var Currency = require("./Currency.js");
var IString = require("./IString.js");
/**
* @class
* Create a new number formatter instance. Locales differ in the way that digits
* in a formatted number are grouped, in the way the decimal character is represented,
* etc. Use this formatter to get it right for any locale.<p>
*
* This formatter can format plain numbers, currency amounts, and percentage amounts.<p>
*
* As with all formatters, the recommended
* practice is to create one formatter and use it multiple times to format various
* numbers.<p>
*
* The options can contain any of the following properties:
*
* <ul>
* <li><i>locale</i> - use the conventions of the specified locale when figuring out how to
* format a number.
* <li><i>type</i> - the type of this formatter. Valid values are "number", "currency", or
* "percentage". If this property is not specified, the default is "number".
* <li><i>currency</i> - the ISO 4217 3-letter currency code to use when the formatter type
* is "currency". This property is required for currency formatting. If the type property
* is "currency" and the currency property is not specified, the constructor will throw a
* an exception.
* <li><i>maxFractionDigits</i> - the maximum number of digits that should appear in the
* formatted output after the decimal. A value of -1 means unlimited, and 0 means only print
* the integral part of the number.
* <li><i>minFractionDigits</i> - the minimum number of fractional digits that should
* appear in the formatted output. If the number does not have enough fractional digits
* to reach this minimum, the number will be zero-padded at the end to get to the limit.
* If the type of the formatter is "currency" and this
* property is not specified, then the minimum fraction digits is set to the normal number
* of digits used with that currency, which is almost always 0, 2, or 3 digits.
* <li><i>significantDigits</i> - specify that max number of significant digits in the
* formatted output. This applies before and after the decimal point. The amount is
* rounded according to the rounding mode specified, or the rounding mode as given in
* the locale information. If the significant digits and the max or min fraction digits
* are both specified, this formatter will attempt to honour them both by choosing the
* one that is smaller if there is a conflict. For example, if the max fraction digits
* is 6 and the significant digits is 5 and the number to be formatted has a long
* fraction, it will only format 5 digits. The default is "unlimited digits", which means
* to format as many digits as the javascript engine can represent internally (usually
* around 13-15 or so on a 64-bit machine).
* <li><i>useNative</i> - the flag used to determaine whether to use the native script settings
* for formatting the numbers .
* <li><i>roundingMode</i> - When the maxFractionDigits or maxIntegerDigits is specified,
* this property governs how the least significant digits are rounded to conform to that
* maximum. The value of this property is a string with one of the following values:
* <ul>
* <li><i>up</i> - round away from zero
* <li><i>down</i> - round towards zero. This has the effect of truncating the number
* <li><i>ceiling</i> - round towards positive infinity
* <li><i>floor</i> - round towards negative infinity
* <li><i>halfup</i> - round towards nearest neighbour. If equidistant, round up.
* <li><i>halfdown</i> - round towards nearest neighbour. If equidistant, round down.
* <li><i>halfeven</i> - round towards nearest neighbour. If equidistant, round towards the even neighbour
* <li><i>halfodd</i> - round towards nearest neighbour. If equidistant, round towards the odd neighbour
* </ul>
* When the type of the formatter is "currency" and the <i>roundingMode</i> property is not
* set, then the standard legal rounding rules for the locale are followed. If the type
* is "number" or "percentage" and the <i>roundingMode</i> property is not set, then the
* default mode is "halfdown".</i>.
*
* <li><i>style</i> - When the type of this formatter is "currency", the currency amount
* can be formatted in the following styles: "common" and "iso". The common style is the
* one commonly used in every day writing where the currency unit is represented using a
* symbol. eg. "$57.35" for fifty-seven dollars and thirty five cents. The iso style is
* the international style where the currency unit is represented using the ISO 4217 code.
* eg. "USD 57.35" for the same amount. The default is "common" style if the style is
* not specified.<br><br>
* When the type of this formatter is "number", the style can be one of the following:
* <ul>
* <li><i>standard - format a fully specified floating point number properly for the locale
* <li><i>scientific</i> - use scientific notation for all numbers. That is, 1 integral
* digit, followed by a number of fractional digits, followed by an "e" which denotes
* exponentiation, followed digits which give the power of 10 in the exponent.
* <li><i>native</i> - format a floating point number using the native digits and
* formatting symbols for the script of the locale.
* <li><i>nogrouping</i> - format a floating point number without grouping digits for
* the integral portion of the number
* </ul>
* Note that if you specify a maximum number
* of integral digits, the formatter with a standard style will give you standard
* formatting for smaller numbers and scientific notation for larger numbers. The default
* is standard style if this is not specified.
*
* <li><i>onLoad</i> - a callback function to call when the format data is fully
* loaded. When the onLoad option is given, this class will attempt to
* load any missing locale data using the ilib loader callback.
* When the constructor is done (even if the data is already preassembled), the
* onLoad function is called with the current instance as a parameter, so this
* callback can be used with preassembled or dynamic loading or a mix of the two.
*
* <li>sync - tell whether to load any missing locale data synchronously or
* asynchronously. If this option is given as "false", then the "onLoad"
* callback must be given, as the instance returned from this constructor will
* not be usable for a while.
*
* <li><i>loadParams</i> - an object containing parameters to pass to the
* loader callback function when locale data is missing. The parameters are not
* interpretted or modified in any way. They are simply passed along. The object
* may contain any property/value pairs as long as the calling code is in
* agreement with the loader callback function as to what those parameters mean.
* </ul>
* <p>
*
*
* @constructor
* @param {Object.<string,*>} options A set of options that govern how the formatter will behave
*/
var NumFmt = function (options) {
var sync = true;
this.locale = new Locale();
/**
* @private
* @type {string}
*/
this.type = "number";
var loadParams = undefined;
if (options) {
if (options.locale) {
this.locale = (typeof (options.locale) === 'string') ? new Locale(options.locale) : options.locale;
}
if (options.type) {
if (options.type === 'number' ||
options.type === 'currency' ||
options.type === 'percentage') {
this.type = options.type;
}
}
if (options.currency) {
/**
* @private
* @type {string}
*/
this.currency = options.currency;
}
if (typeof (options.maxFractionDigits) !== 'undefined') {
/**
* @private
* @type {number|undefined}
*/
this.maxFractionDigits = Number(options.maxFractionDigits);
}
if (typeof (options.minFractionDigits) !== 'undefined') {
/**
* @private
* @type {number|undefined}
*/
this.minFractionDigits = Number(options.minFractionDigits);
// enforce the limits to avoid JS exceptions
if (this.minFractionDigits < 0) {
this.minFractionDigits = 0;
}
if (this.minFractionDigits > 20) {
this.minFractionDigits = 20;
}
}
if (typeof (options.significantDigits) !== 'undefined') {
/**
* @private
* @type {number|undefined}
*/
this.significantDigits = Number(options.significantDigits);
// enforce the limits to avoid JS exceptions
if (this.significantDigits < 1) {
this.significantDigits = 1;
}
if (this.significantDigits > 20) {
this.significantDigits = 20;
}
}
if (options.style) {
/**
* @private
* @type {string}
*/
this.style = options.style;
}
if (typeof(options.useNative) === 'boolean') {
/**
* @private
* @type {boolean}
* */
this.useNative = options.useNative;
}
/**
* @private
* @type {string}
*/
this.roundingMode = options.roundingMode;
if (typeof(options.sync) === 'boolean') {
sync = options.sync;
}
loadParams = options.loadParams;
}
/**
* @private
* @type {LocaleInfo|undefined}
*/
this.localeInfo = undefined;
new LocaleInfo(this.locale, {
sync: sync,
loadParams: loadParams,
onLoad: ilib.bind(this, function (li) {
/**
* @private
* @type {LocaleInfo|undefined}
*/
this.localeInfo = li;
if (this.type === "number") {
this.templateNegative = new IString(this.localeInfo.getNegativeNumberFormat() || "-{n}");
} else if (this.type === "currency") {
var templates;
if (!this.currency || typeof (this.currency) != 'string') {
throw "A currency property is required in the options to the number formatter constructor when the type property is set to currency.";
}
new Currency({
locale: this.locale,
code: this.currency,
sync: sync,
loadParams: loadParams,
onLoad: ilib.bind(this, function (cur) {
this.currencyInfo = cur;
if (this.style !== "common" && this.style !== "iso") {
this.style = "common";
}
if (typeof(this.maxFractionDigits) !== 'number' && typeof(this.minFractionDigits) !== 'number') {
this.minFractionDigits = this.maxFractionDigits = this.currencyInfo.getFractionDigits();
}
templates = this.localeInfo.getCurrencyFormats();
this.template = new IString(templates[this.style] || templates.common);
this.templateNegative = new IString(templates[this.style + "Negative"] || templates["commonNegative"]);
this.sign = (this.style === "iso") ? this.currencyInfo.getCode() : this.currencyInfo.getSign();
if (!this.roundingMode) {
this.roundingMode = this.currencyInfo && this.currencyInfo.roundingMode;
}
this._init();
if (options && typeof (options.onLoad) === 'function') {
options.onLoad(this);
}
})
});
return;
} else if (this.type === "percentage") {
this.template = new IString(this.localeInfo.getPercentageFormat() || "{n}%");
this.templateNegative = new IString(this.localeInfo.getNegativePercentageFormat() || this.localeInfo.getNegativeNumberFormat() + "%");
}
this._init();
if (options && typeof (options.onLoad) === 'function') {
options.onLoad(this);
}
})
});
};
/**
* Return an array of available locales that this formatter can format
* @static
* @return {Array.<Locale>|undefined} an array of available locales
*/
NumFmt.getAvailableLocales = function () {
return undefined;
};
/**
* @private
* @const
* @type string
*/
NumFmt.zeros = "0000000000000000000000000000000000000000000000000000000000000000000000";
NumFmt.prototype = {
/**
* Return true if this formatter uses native digits to format the number. If the useNative
* option is given to the constructor, then this flag will be honoured. If the useNative
* option is not given to the constructor, this this formatter will use native digits if
* the locale typically uses native digits.
*
* @return {boolean} true if this formatter will format with native digits, false otherwise
*/
getUseNative: function() {
if (typeof(this.useNative) === "boolean") {
return this.useNative;
}
return (this.localeInfo.getDigitsStyle() === "native");
},
/**
* @private
*/
_init: function () {
if (this.maxFractionDigits < this.minFractionDigits) {
this.minFractionDigits = this.maxFractionDigits;
}
if (!this.roundingMode) {
this.roundingMode = this.localeInfo.getRoundingMode();
}
if (!this.roundingMode) {
this.roundingMode = "halfdown";
}
// set up the function, so we only have to figure it out once
// and not every time we do format()
this.round = MathUtils[this.roundingMode];
if (!this.round) {
this.roundingMode = "halfdown";
this.round = MathUtils[this.roundingMode];
}
if (this.style === "nogrouping") {
this.prigroupSize = this.secgroupSize = 0;
} else {
this.prigroupSize = this.localeInfo.getPrimaryGroupingDigits();
this.secgroupSize = this.localeInfo.getSecondaryGroupingDigits();
this.groupingSeparator = this.getUseNative() ? this.localeInfo.getNativeGroupingSeparator() : this.localeInfo.getGroupingSeparator();
}
this.decimalSeparator = this.getUseNative() ? this.localeInfo.getNativeDecimalSeparator() : this.localeInfo.getDecimalSeparator();
if (this.getUseNative()) {
var nd = this.localeInfo.getNativeDigits() || this.localeInfo.getDigits();
if (nd) {
this.digits = nd.split("");
}
}
this.exponentSymbol = this.localeInfo.getExponential() || "e";
},
/**
* Apply the constraints used in the current formatter to the given number. This will
* will apply the maxFractionDigits, significantDigits, and rounding mode
* constraints and return the result. The result is further
* manipulated in the format method to produce the final formatted number string.
* This method is intended for use by code that needs to use the same number that
* this formatter instance uses for formatting before that number is turned into a
* formatted string.
*
* @param {number} num the number to constrain
* @returns {number} the number with the constraints applied to it
*/
constrain: function(num) {
var parts = ("" + num).split("."),
result = num;
// only apply the either significantDigits or the maxFractionDigits -- whichever results in a shorter fractional part
if ((typeof(this.significantDigits) !== 'undefined' && this.significantDigits > 0) &&
(typeof(this.maxFractionDigits) === 'undefined' || this.maxFractionDigits < 0 ||
parts[0].length + this.maxFractionDigits > this.significantDigits)) {
result = MathUtils.significant(result, this.significantDigits, this.round);
}
if (typeof(this.maxFractionDigits) !== 'undefined' && this.maxFractionDigits > -1) {
result = MathUtils.shiftDecimal(this.round(MathUtils.shiftDecimal(result, this.maxFractionDigits)), -this.maxFractionDigits);
}
return result;
},
/**
* Format the number using scientific notation as a positive number. Negative
* formatting to be applied later.
* @private
* @param {number} num the number to format
* @return {string} the formatted number
*/
_formatScientific: function (num) {
var n = new Number(num);
var formatted;
var str = n.toExponential(),
parts = str.split("e"),
significant = parts[0],
exponent = parts[1],
numparts,
integral,
fraction;
if (this.maxFractionDigits > 0 || this.significantDigits > 0) {
// if there is a max fraction digits setting, round the fraction to
// the right length first by dividing or multiplying by powers of 10.
// manipulate the fraction digits so as to
// avoid the rounding errors of floating point numbers
var maxDigits = (this.maxFractionDigits || 25) + 1;
if (this.significantDigits > 0) {
maxDigits = Math.min(maxDigits, this.significantDigits);
}
significant = MathUtils.significant(Number(significant), maxDigits, this.round);
}
numparts = ("" + significant).split(".");
integral = numparts[0];
fraction = numparts[1];
if (typeof(this.maxFractionDigits) !== 'undefined') {
fraction = fraction.substring(0, this.maxFractionDigits);
}
if (typeof(this.minFractionDigits) !== 'undefined') {
fraction = JSUtils.pad(fraction || "", this.minFractionDigits, true);
}
formatted = integral;
if (fraction.length) {
formatted += this.decimalSeparator + fraction;
}
formatted += this.exponentSymbol + exponent;
return formatted;
},
/**
* Formats the number as a positive number. Negative formatting to be applied later.
* @private
* @param {number} num the number to format
* @return {string} the formatted number
*/
_formatStandard: function (num) {
var i;
var k;
var parts,
integral,
fraction,
cycle,
formatted;
num = Math.abs(this.constrain(num));
parts = ("" + num).split(".");
integral = parts[0].toString();
fraction = parts[1];
if (this.minFractionDigits > 0) {
fraction = JSUtils.pad(fraction || "", this.minFractionDigits, true);
}
if (this.secgroupSize > 0) {
if (integral.length > this.prigroupSize) {
var size1 = this.prigroupSize;
var size2 = integral.length;
var size3 = size2 - size1;
integral = integral.slice(0, size3) + this.groupingSeparator + integral.slice(size3);
var num_sec = integral.substring(0, integral.indexOf(this.groupingSeparator));
k = num_sec.length;
while (k > this.secgroupSize) {
var secsize1 = this.secgroupSize;
var secsize2 = num_sec.length;
var secsize3 = secsize2 - secsize1;
integral = integral.slice(0, secsize3) + this.groupingSeparator + integral.slice(secsize3);
num_sec = integral.substring(0, integral.indexOf(this.groupingSeparator));
k = num_sec.length;
}
}
formatted = integral;
} else if (this.prigroupSize !== 0) {
cycle = MathUtils.mod(integral.length - 1, this.prigroupSize);
formatted = "";
for (i = 0; i < integral.length - 1; i++) {
formatted += integral.charAt(i);
if (cycle === 0) {
formatted += this.groupingSeparator;
}
cycle = MathUtils.mod(cycle - 1, this.prigroupSize);
}
formatted += integral.charAt(integral.length - 1);
} else {
formatted = integral;
}
if (fraction &&
((typeof(this.maxFractionDigits) === 'undefined' && typeof(this.significantDigits) === 'undefined') ||
this.maxFractionDigits > 0 || this.significantDigits > 0)) {
formatted += this.decimalSeparator;
formatted += fraction;
}
if (this.digits) {
formatted = JSUtils.mapString(formatted, this.digits);
}
return formatted;
},
/**
* Format a number according to the settings of this number formatter instance.
* @param num {number|string|INumber|Number} a floating point number to format
* @return {string} a string containing the formatted number
*/
format: function (num) {
var formatted, n;
if (typeof (num) === 'undefined') {
return "";
}
// convert to a real primitive number type
n = Number(num);
if (this.type === "number") {
formatted = (this.style === "scientific") ?
this._formatScientific(n) :
this._formatStandard(n);
if (num < 0) {
formatted = this.templateNegative.format({n: formatted});
}
} else {
formatted = this._formatStandard(n);
var template = (n < 0) ? this.templateNegative : this.template;
formatted = template.format({
n: formatted,
s: this.sign
});
}
return formatted;
},
/**
* Return the type of formatter. Valid values are "number", "currency", and
* "percentage".
*
* @return {string} the type of formatter
*/
getType: function () {
return this.type;
},
/**
* Return the locale for this formatter instance.
* @return {Locale} the locale instance for this formatter
*/
getLocale: function () {
return this.locale;
},
/**
* Returns true if this formatter groups together digits in the integral
* portion of a number, based on the options set up in the constructor. In
* most western European cultures, this means separating every 3 digits
* of the integral portion of a number with a particular character.
*
* @return {boolean} true if this formatter groups digits in the integral
* portion of the number
*/
isGroupingUsed: function () {
return (this.groupingSeparator !== 'undefined' && this.groupingSeparator.length > 0);
},
/**
* Returns the maximum fraction digits set up in the constructor.
*
* @return {number} the maximum number of fractional digits this
* formatter will format, or -1 for no maximum
*/
getMaxFractionDigits: function () {
return typeof (this.maxFractionDigits) !== 'undefined' ? this.maxFractionDigits : -1;
},
/**
* Returns the minimum fraction digits set up in the constructor. If
* the formatter has the type "currency", then the minimum fraction
* digits is the amount of digits that is standard for the currency
* in question unless overridden in the options to the constructor.
*
* @return {number} the minimum number of fractional digits this
* formatter will format, or -1 for no minimum
*/
getMinFractionDigits: function () {
return typeof (this.minFractionDigits) !== 'undefined' ? this.minFractionDigits : -1;
},
/**
* Returns the significant digits set up in the constructor.
*
* @return {number} the number of significant digits this
* formatter will format, or -1 for no minimum
*/
getSignificantDigits: function () {
return typeof (this.significantDigits) !== 'undefined' ? this.significantDigits : -1;
},
/**
* Returns the ISO 4217 code for the currency that this formatter formats.
* IF the typeof this formatter is not "currency", then this method will
* return undefined.
*
* @return {string} the ISO 4217 code for the currency that this formatter
* formats, or undefined if this not a currency formatter
*/
getCurrency: function () {
return this.currencyInfo && this.currencyInfo.getCode();
},
/**
* Returns the rounding mode set up in the constructor. The rounding mode
* controls how numbers are rounded when the integral or fraction digits
* of a number are limited.
*
* @return {string} the name of the rounding mode used in this formatter
*/
getRoundingMode: function () {
return this.roundingMode;
},
/**
* If this formatter is a currency formatter, then the style determines how the
* currency is denoted in the formatted output. This method returns the style
* that this formatter will produce. (See the constructor comment for more about
* the styles.)
* @return {string} the name of the style this formatter will use to format
* currency amounts, or "undefined" if this formatter is not a currency formatter
*/
getStyle: function () {
return this.style;
}
};
module.exports = NumFmt;
Source