1 /*
  2  * NumFmt.js - Number formatter definition
  3  *
  4  * Copyright © 2012-2015, 2018 JEDLSoft
  5  *
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  *
 10  *     http://www.apache.org/licenses/LICENSE-2.0
 11  *
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  *
 16  * See the License for the specific language governing permissions and
 17  * limitations under the License.
 18  */
 19 
 20 /*
 21 !depends
 22 ilib.js
 23 Locale.js
 24 LocaleInfo.js
 25 MathUtils.js
 26 Currency.js
 27 IString.js
 28 JSUtils.js
 29 */
 30 
 31 // !data localeinfo currency
 32 
 33 var ilib = require("./ilib.js");
 34 var JSUtils = require("./JSUtils.js");
 35 var MathUtils = require("./MathUtils.js");
 36 
 37 var Locale = require("./Locale.js");
 38 var LocaleInfo = require("./LocaleInfo.js");
 39 var Currency = require("./Currency.js");
 40 var IString = require("./IString.js");
 41 
 42 /**
 43  * @class
 44  * Create a new number formatter instance. Locales differ in the way that digits
 45  * in a formatted number are grouped, in the way the decimal character is represented,
 46  * etc. Use this formatter to get it right for any locale.<p>
 47  *
 48  * This formatter can format plain numbers, currency amounts, and percentage amounts.<p>
 49  *
 50  * As with all formatters, the recommended
 51  * practice is to create one formatter and use it multiple times to format various
 52  * numbers.<p>
 53  *
 54  * The options can contain any of the following properties:
 55  *
 56  * <ul>
 57  * <li><i>locale</i> - use the conventions of the specified locale when figuring out how to
 58  * format a number.
 59  * <li><i>type</i> - the type of this formatter. Valid values are "number", "currency", or
 60  * "percentage". If this property is not specified, the default is "number".
 61  * <li><i>currency</i> - the ISO 4217 3-letter currency code to use when the formatter type
 62  * is "currency". This property is required for currency formatting. If the type property
 63  * is "currency" and the currency property is not specified, the constructor will throw a
 64  * an exception.
 65  * <li><i>maxFractionDigits</i> - the maximum number of digits that should appear in the
 66  * formatted output after the decimal. A value of -1 means unlimited, and 0 means only print
 67  * the integral part of the number.
 68  * <li><i>minFractionDigits</i> - the minimum number of fractional digits that should
 69  * appear in the formatted output. If the number does not have enough fractional digits
 70  * to reach this minimum, the number will be zero-padded at the end to get to the limit.
 71  * If the type of the formatter is "currency" and this
 72  * property is not specified, then the minimum fraction digits is set to the normal number
 73  * of digits used with that currency, which is almost always 0, 2, or 3 digits.
 74  * <li><i>significantDigits</i> - specify that max number of significant digits in the
 75  * formatted output. This applies before and after the decimal point. The amount is
 76  * rounded according to the rounding mode specified, or the rounding mode as given in
 77  * the locale information. If the significant digits and the max or min fraction digits
 78  * are both specified, this formatter will attempt to honour them both by choosing the
 79  * one that is smaller if there is a conflict. For example, if the max fraction digits
 80  * is 6 and the significant digits is 5 and the number to be formatted has a long
 81  * fraction, it will only format 5 digits. The default is "unlimited digits", which means
 82  * to format as many digits as the javascript engine can represent internally (usually
 83  * around 13-15 or so on a 64-bit machine).
 84  * <li><i>useNative</i> - the flag used to determaine whether to use the native script settings
 85  * for formatting the numbers .
 86  * <li><i>roundingMode</i> - When the maxFractionDigits or maxIntegerDigits is specified,
 87  * this property governs how the least significant digits are rounded to conform to that
 88  * maximum. The value of this property is a string with one of the following values:
 89  * <ul>
 90  *   <li><i>up</i> - round away from zero
 91  *   <li><i>down</i> - round towards zero. This has the effect of truncating the number
 92  *   <li><i>ceiling</i> - round towards positive infinity
 93  *   <li><i>floor</i> - round towards negative infinity
 94  *   <li><i>halfup</i> - round towards nearest neighbour. If equidistant, round up.
 95  *   <li><i>halfdown</i> - round towards nearest neighbour. If equidistant, round down.
 96  *   <li><i>halfeven</i> - round towards nearest neighbour. If equidistant, round towards the even neighbour
 97  *   <li><i>halfodd</i> - round towards nearest neighbour. If equidistant, round towards the odd neighbour
 98  * </ul>
 99  * When the type of the formatter is "currency" and the <i>roundingMode</i> property is not
100  * set, then the standard legal rounding rules for the locale are followed. If the type
101  * is "number" or "percentage" and the <i>roundingMode</i> property is not set, then the
102  * default mode is "halfdown".</i>.
103  *
104  * <li><i>style</i> - When the type of this formatter is "currency", the currency amount
105  * can be formatted in the following styles: "common" and "iso". The common style is the
106  * one commonly used in every day writing where the currency unit is represented using a
107  * symbol. eg. "$57.35" for fifty-seven dollars and thirty five cents. The iso style is
108  * the international style where the currency unit is represented using the ISO 4217 code.
109  * eg. "USD 57.35" for the same amount. The default is "common" style if the style is
110  * not specified.<p>
111  *
112  * When the type of this formatter is "number", the style can be one of the following:
113  * <ul>
114  *   <li><i>standard - format a fully specified floating point number properly for the locale
115  *   <li><i>scientific</i> - use scientific notation for all numbers. That is, 1 integral
116  *   digit, followed by a number of fractional digits, followed by an "e" which denotes
117  *   exponentiation, followed digits which give the power of 10 in the exponent.
118  *   <li><i>native</i> - format a floating point number using the native digits and
119  *   formatting symbols for the script of the locale.
120  *   <li><i>nogrouping</i> - format a floating point number without grouping digits for
121  *   the integral portion of the number
122  * </ul>
123  * Note that if you specify a maximum number
124  * of integral digits, the formatter with a standard style will give you standard
125  * formatting for smaller numbers and scientific notation for larger numbers. The default
126  * is standard style if this is not specified.
127  *
128  * <li><i>onLoad</i> - a callback function to call when the format data is fully
129  * loaded. When the onLoad option is given, this class will attempt to
130  * load any missing locale data using the ilib loader callback.
131  * When the constructor is done (even if the data is already preassembled), the
132  * onLoad function is called with the current instance as a parameter, so this
133  * callback can be used with preassembled or dynamic loading or a mix of the two.
134  *
135  * <li>sync - tell whether to load any missing locale data synchronously or
136  * asynchronously. If this option is given as "false", then the "onLoad"
137  * callback must be given, as the instance returned from this constructor will
138  * not be usable for a while.
139  *
140  * <li><i>loadParams</i> - an object containing parameters to pass to the
141  * loader callback function when locale data is missing. The parameters are not
142  * interpretted or modified in any way. They are simply passed along. The object
143  * may contain any property/value pairs as long as the calling code is in
144  * agreement with the loader callback function as to what those parameters mean.
145  * </ul>
146  * <p>
147  *
148  *
149  * @constructor
150  * @param {Object.<string,*>} options A set of options that govern how the formatter will behave
151  */
152 var NumFmt = function (options) {
153     var sync = true;
154     this.locale = new Locale();
155     /**
156      * @private
157      * @type {string}
158      */
159     this.type = "number";
160     var loadParams = undefined;
161 
162     if (options) {
163         if (options.locale) {
164             this.locale = (typeof (options.locale) === 'string') ? new Locale(options.locale) : options.locale;
165         }
166 
167         if (options.type) {
168             if (options.type === 'number' ||
169                 options.type === 'currency' ||
170                 options.type === 'percentage') {
171                 this.type = options.type;
172             }
173         }
174 
175         if (options.currency) {
176             /**
177              * @private
178              * @type {string}
179              */
180             this.currency = options.currency;
181         }
182 
183         if (typeof (options.maxFractionDigits) !== 'undefined') {
184             /**
185              * @private
186              * @type {number|undefined}
187              */
188             this.maxFractionDigits = Number(options.maxFractionDigits);
189         }
190         if (typeof (options.minFractionDigits) !== 'undefined') {
191             /**
192              * @private
193              * @type {number|undefined}
194              */
195             this.minFractionDigits = Number(options.minFractionDigits);
196             // enforce the limits to avoid JS exceptions
197             if (this.minFractionDigits < 0) {
198                 this.minFractionDigits = 0;
199             }
200             if (this.minFractionDigits > 20) {
201                 this.minFractionDigits = 20;
202             }
203         }
204         if (typeof (options.significantDigits) !== 'undefined') {
205             /**
206              * @private
207              * @type {number|undefined}
208              */
209             this.significantDigits = Number(options.significantDigits);
210             // enforce the limits to avoid JS exceptions
211             if (this.significantDigits < 1) {
212                 this.significantDigits = 1;
213             }
214             if (this.significantDigits > 20) {
215                 this.significantDigits = 20;
216             }
217         }
218         if (options.style) {
219             /**
220              * @private
221              * @type {string}
222              */
223             this.style = options.style;
224         }
225         if (typeof(options.useNative) === 'boolean') {
226             /**
227              * @private
228              * @type {boolean}
229              * */
230             this.useNative = options.useNative;
231         }
232         /**
233          * @private
234          * @type {string}
235          */
236         this.roundingMode = options.roundingMode;
237 
238         if (typeof(options.sync) === 'boolean') {
239             sync = options.sync;
240         }
241 
242         loadParams = options.loadParams;
243     }
244 
245     /**
246      * @private
247      * @type {LocaleInfo|undefined}
248      */
249     this.localeInfo = undefined;
250 
251     new LocaleInfo(this.locale, {
252         sync: sync,
253         loadParams: loadParams,
254         onLoad: ilib.bind(this, function (li) {
255             /**
256              * @private
257              * @type {LocaleInfo|undefined}
258              */
259             this.localeInfo = li;
260 
261             if (this.type === "number") {
262                 this.templateNegative = new IString(this.localeInfo.getNegativeNumberFormat() || "-{n}");
263             } else if (this.type === "currency") {
264                 var templates;
265 
266                 if (!this.currency || typeof (this.currency) != 'string') {
267                     throw "A currency property is required in the options to the number formatter constructor when the type property is set to currency.";
268                 }
269 
270                 new Currency({
271                     locale: this.locale,
272                     code: this.currency,
273                     sync: sync,
274                     loadParams: loadParams,
275                     onLoad: ilib.bind(this, function (cur) {
276                         this.currencyInfo = cur;
277                         if (this.style !== "common" && this.style !== "iso") {
278                             this.style = "common";
279                         }
280 
281                         if (typeof(this.maxFractionDigits) !== 'number' && typeof(this.minFractionDigits) !== 'number') {
282                             this.minFractionDigits = this.maxFractionDigits = this.currencyInfo.getFractionDigits();
283                         }
284 
285                         templates = this.localeInfo.getCurrencyFormats();
286                         this.template = new IString(templates[this.style] || templates.common);
287                         this.templateNegative = new IString(templates[this.style + "Negative"] || templates["commonNegative"]);
288                         this.sign = (this.style === "iso") ? this.currencyInfo.getCode() : this.currencyInfo.getSign();
289 
290                         if (!this.roundingMode) {
291                             this.roundingMode = this.currencyInfo && this.currencyInfo.roundingMode;
292                         }
293 
294                         this._init();
295 
296                         if (options && typeof (options.onLoad) === 'function') {
297                             options.onLoad(this);
298                         }
299                     })
300                 });
301                 return;
302             } else if (this.type === "percentage") {
303                 this.template =  new IString(this.localeInfo.getPercentageFormat() || "{n}%");
304                 this.templateNegative = new IString(this.localeInfo.getNegativePercentageFormat() || this.localeInfo.getNegativeNumberFormat() + "%");
305             }
306 
307             this._init();
308 
309             if (options && typeof (options.onLoad) === 'function') {
310                 options.onLoad(this);
311             }
312         })
313     });
314 };
315 
316 /**
317  * Return an array of available locales that this formatter can format
318  * @static
319  * @return {Array.<Locale>|undefined} an array of available locales
320  */
321 NumFmt.getAvailableLocales = function () {
322     return undefined;
323 };
324 
325 /**
326  * @private
327  * @const
328  * @type string
329  */
330 NumFmt.zeros = "0000000000000000000000000000000000000000000000000000000000000000000000";
331 
332 NumFmt.prototype = {
333     /**
334      * Return true if this formatter uses native digits to format the number. If the useNative
335      * option is given to the constructor, then this flag will be honoured. If the useNative
336      * option is not given to the constructor, this this formatter will use native digits if
337      * the locale typically uses native digits.
338      *
339      *  @return {boolean} true if this formatter will format with native digits, false otherwise
340      */
341     getUseNative: function() {
342         if (typeof(this.useNative) === "boolean") {
343             return this.useNative;
344         }
345         return (this.localeInfo.getDigitsStyle() === "native");
346     },
347 
348     /**
349      * @private
350      */
351     _init: function () {
352         if (this.maxFractionDigits < this.minFractionDigits) {
353             this.minFractionDigits = this.maxFractionDigits;
354         }
355 
356         if (!this.roundingMode) {
357             this.roundingMode = this.localeInfo.getRoundingMode();
358         }
359 
360         if (!this.roundingMode) {
361             this.roundingMode = "halfdown";
362         }
363 
364         // set up the function, so we only have to figure it out once
365         // and not every time we do format()
366         this.round = MathUtils[this.roundingMode];
367         if (!this.round) {
368             this.roundingMode = "halfdown";
369             this.round = MathUtils[this.roundingMode];
370         }
371 
372         if (this.style === "nogrouping") {
373             this.prigroupSize = this.secgroupSize = 0;
374         } else {
375             this.prigroupSize = this.localeInfo.getPrimaryGroupingDigits();
376             this.secgroupSize = this.localeInfo.getSecondaryGroupingDigits();
377             this.groupingSeparator = this.getUseNative() ? this.localeInfo.getNativeGroupingSeparator() : this.localeInfo.getGroupingSeparator();
378         }
379         this.decimalSeparator = this.getUseNative() ? this.localeInfo.getNativeDecimalSeparator() : this.localeInfo.getDecimalSeparator();
380 
381         if (this.getUseNative()) {
382             var nd = this.localeInfo.getNativeDigits() || this.localeInfo.getDigits();
383             if (nd) {
384                 this.digits = nd.split("");
385             }
386         }
387 
388         this.exponentSymbol = this.localeInfo.getExponential() || "e";
389     },
390 
391     /**
392      * Apply the constraints used in the current formatter to the given number. This will
393      * will apply the maxFractionDigits, significantDigits, and rounding mode
394      * constraints and return the result. The result is further
395      * manipulated in the format method to produce the final formatted number string.
396      * This method is intended for use by code that needs to use the same number that
397      * this formatter instance uses for formatting before that number is turned into a
398      * formatted string.
399      *
400      * @param {number} num the number to constrain
401      * @returns {number} the number with the constraints applied to it
402      */
403     constrain: function(num) {
404         var parts = ("" + num).split("."),
405             result = num;
406 
407         // only apply the either significantDigits or the maxFractionDigits -- whichever results in a shorter fractional part
408         if ((typeof(this.significantDigits) !== 'undefined' && this.significantDigits > 0) &&
409             (typeof(this.maxFractionDigits) === 'undefined' || this.maxFractionDigits < 0 ||
410                 parts[0].length + this.maxFractionDigits > this.significantDigits)) {
411             result = MathUtils.significant(result, this.significantDigits, this.round);
412         }
413 
414         if (typeof(this.maxFractionDigits) !== 'undefined' && this.maxFractionDigits > -1) {
415             result = MathUtils.shiftDecimal(this.round(MathUtils.shiftDecimal(result, this.maxFractionDigits)), -this.maxFractionDigits);
416         }
417 
418         return result;
419     },
420 
421     /**
422      * Format the number using scientific notation as a positive number. Negative
423      * formatting to be applied later.
424      * @private
425      * @param {number} num the number to format
426      * @return {string} the formatted number
427      */
428     _formatScientific: function (num) {
429         var n = new Number(num);
430         var formatted;
431 
432         var str = n.toExponential(),
433             parts = str.split("e"),
434             significant = parts[0],
435             exponent = parts[1],
436             numparts,
437             integral,
438             fraction;
439 
440         if (this.maxFractionDigits > 0 || this.significantDigits > 0) {
441             // if there is a max fraction digits setting, round the fraction to
442             // the right length first by dividing or multiplying by powers of 10.
443             // manipulate the fraction digits so as to
444             // avoid the rounding errors of floating point numbers
445             var maxDigits = (this.maxFractionDigits || 25) + 1;
446             if (this.significantDigits > 0) {
447                 maxDigits = Math.min(maxDigits, this.significantDigits);
448             }
449             significant = MathUtils.significant(Number(significant), maxDigits, this.round);
450         }
451         numparts = ("" + significant).split(".");
452         integral = numparts[0];
453         fraction = numparts[1];
454 
455         if (typeof(this.maxFractionDigits) !== 'undefined') {
456             fraction = fraction.substring(0, this.maxFractionDigits);
457         }
458         if (typeof(this.minFractionDigits) !== 'undefined') {
459             fraction = JSUtils.pad(fraction || "", this.minFractionDigits, true);
460         }
461         formatted = integral;
462         if (fraction.length) {
463             formatted += this.decimalSeparator + fraction;
464         }
465         formatted += this.exponentSymbol + exponent;
466         return formatted;
467     },
468 
469     /**
470      * Formats the number as a positive number. Negative formatting to be applied later.
471      * @private
472      * @param {number} num the number to format
473      * @return {string} the formatted number
474      */
475     _formatStandard: function (num) {
476         var i;
477         var k;
478 
479         var parts,
480             integral,
481             fraction,
482             cycle,
483             formatted;
484 
485         num = Math.abs(this.constrain(num));
486 
487         parts = ("" + num).split(".");
488         integral = parts[0].toString();
489         fraction = parts[1];
490 
491         if (this.minFractionDigits > 0) {
492             fraction = JSUtils.pad(fraction || "", this.minFractionDigits, true);
493         }
494 
495         if (this.secgroupSize > 0) {
496             if (integral.length > this.prigroupSize) {
497                 var size1 = this.prigroupSize;
498                 var size2 = integral.length;
499                 var size3 = size2 - size1;
500                 integral = integral.slice(0, size3) + this.groupingSeparator + integral.slice(size3);
501                 var num_sec = integral.substring(0, integral.indexOf(this.groupingSeparator));
502                 k = num_sec.length;
503                 while (k > this.secgroupSize) {
504                     var secsize1 = this.secgroupSize;
505                     var secsize2 = num_sec.length;
506                     var secsize3 = secsize2 - secsize1;
507                     integral = integral.slice(0, secsize3) + this.groupingSeparator + integral.slice(secsize3);
508                     num_sec = integral.substring(0, integral.indexOf(this.groupingSeparator));
509                     k = num_sec.length;
510                 }
511             }
512 
513             formatted = integral;
514         } else if (this.prigroupSize !== 0) {
515             cycle = MathUtils.mod(integral.length - 1, this.prigroupSize);
516 
517             formatted = "";
518 
519             for (i = 0; i < integral.length - 1; i++) {
520                 formatted += integral.charAt(i);
521                 if (cycle === 0) {
522                     formatted += this.groupingSeparator;
523                 }
524                 cycle = MathUtils.mod(cycle - 1, this.prigroupSize);
525             }
526             formatted += integral.charAt(integral.length - 1);
527         } else {
528             formatted = integral;
529         }
530 
531         if (fraction &&
532             ((typeof(this.maxFractionDigits) === 'undefined' && typeof(this.significantDigits) === 'undefined') ||
533               this.maxFractionDigits > 0 || this.significantDigits > 0)) {
534             formatted += this.decimalSeparator;
535             formatted += fraction;
536         }
537 
538         if (this.digits) {
539             formatted = JSUtils.mapString(formatted, this.digits);
540         }
541 
542         return formatted;
543     },
544 
545     /**
546      * Format a number according to the settings of this number formatter instance.
547      * @param num {number|string|INumber|Number} a floating point number to format
548      * @return {string} a string containing the formatted number
549      */
550     format: function (num) {
551         var formatted, n;
552 
553         if (typeof (num) === 'undefined') {
554             return "";
555         }
556 
557         // convert to a real primitive number type
558         n = Number(num);
559 
560         if (this.type === "number") {
561             formatted = (this.style === "scientific") ?
562                 this._formatScientific(n) :
563                 this._formatStandard(n);
564 
565             if (num < 0) {
566                 formatted = this.templateNegative.format({n: formatted});
567             }
568         } else {
569             formatted = this._formatStandard(n);
570             var template = (n < 0) ? this.templateNegative : this.template;
571             formatted = template.format({
572                 n: formatted,
573                 s: this.sign
574             });
575         }
576 
577         return formatted;
578     },
579 
580     /**
581      * Return the type of formatter. Valid values are "number", "currency", and
582      * "percentage".
583      *
584      * @return {string} the type of formatter
585      */
586     getType: function () {
587         return this.type;
588     },
589 
590     /**
591      * Return the locale for this formatter instance.
592      * @return {Locale} the locale instance for this formatter
593      */
594     getLocale: function () {
595         return this.locale;
596     },
597 
598     /**
599      * Returns true if this formatter groups together digits in the integral
600      * portion of a number, based on the options set up in the constructor. In
601      * most western European cultures, this means separating every 3 digits
602      * of the integral portion of a number with a particular character.
603      *
604      * @return {boolean} true if this formatter groups digits in the integral
605      * portion of the number
606      */
607     isGroupingUsed: function () {
608         return (this.groupingSeparator !== 'undefined' && this.groupingSeparator.length > 0);
609     },
610 
611     /**
612      * Returns the maximum fraction digits set up in the constructor.
613      *
614      * @return {number} the maximum number of fractional digits this
615      * formatter will format, or -1 for no maximum
616      */
617     getMaxFractionDigits: function () {
618         return typeof (this.maxFractionDigits) !== 'undefined' ? this.maxFractionDigits : -1;
619     },
620 
621     /**
622      * Returns the minimum fraction digits set up in the constructor. If
623      * the formatter has the type "currency", then the minimum fraction
624      * digits is the amount of digits that is standard for the currency
625      * in question unless overridden in the options to the constructor.
626      *
627      * @return {number} the minimum number of fractional digits this
628      * formatter will format, or -1 for no minimum
629      */
630     getMinFractionDigits: function () {
631         return typeof (this.minFractionDigits) !== 'undefined' ? this.minFractionDigits : -1;
632     },
633 
634    /**
635      * Returns the significant digits set up in the constructor.
636      *
637      * @return {number} the number of significant digits this
638      * formatter will format, or -1 for no minimum
639      */
640     getSignificantDigits: function () {
641         return typeof (this.significantDigits) !== 'undefined' ? this.significantDigits : -1;
642     },
643 
644     /**
645      * Returns the ISO 4217 code for the currency that this formatter formats.
646      * IF the typeof this formatter is not "currency", then this method will
647      * return undefined.
648      *
649      * @return {string} the ISO 4217 code for the currency that this formatter
650      * formats, or undefined if this not a currency formatter
651      */
652     getCurrency: function () {
653         return this.currencyInfo && this.currencyInfo.getCode();
654     },
655 
656     /**
657      * Returns the rounding mode set up in the constructor. The rounding mode
658      * controls how numbers are rounded when the integral or fraction digits
659      * of a number are limited.
660      *
661      * @return {string} the name of the rounding mode used in this formatter
662      */
663     getRoundingMode: function () {
664         return this.roundingMode;
665     },
666 
667     /**
668      * If this formatter is a currency formatter, then the style determines how the
669      * currency is denoted in the formatted output. This method returns the style
670      * that this formatter will produce. (See the constructor comment for more about
671      * the styles.)
672      * @return {string} the name of the style this formatter will use to format
673      * currency amounts, or "undefined" if this formatter is not a currency formatter
674      */
675     getStyle: function () {
676         return this.style;
677     }
678 };
679 
680 module.exports = NumFmt;
681