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