| // Copyright 2006 The Closure Library Authors. All Rights Reserved. |
| // |
| // 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. |
| |
| /** |
| * @fileoverview Number format/parse library with locale support. |
| */ |
| |
| |
| /** |
| * Namespace for locale number format functions |
| */ |
| goog.provide('goog.i18n.NumberFormat'); |
| goog.provide('goog.i18n.NumberFormat.CurrencyStyle'); |
| goog.provide('goog.i18n.NumberFormat.Format'); |
| |
| goog.require('goog.asserts'); |
| goog.require('goog.i18n.CompactNumberFormatSymbols'); |
| goog.require('goog.i18n.NumberFormatSymbols'); |
| goog.require('goog.i18n.currency'); |
| goog.require('goog.math'); |
| |
| |
| |
| /** |
| * Constructor of NumberFormat. |
| * @param {number|string} pattern The number that indicates a predefined |
| * number format pattern. |
| * @param {string=} opt_currency Optional international currency |
| * code. This determines the currency code/symbol used in format/parse. If |
| * not given, the currency code for current locale will be used. |
| * @param {number=} opt_currencyStyle currency style, value defined in |
| * goog.i18n.NumberFormat.CurrencyStyle. |
| * @constructor |
| */ |
| goog.i18n.NumberFormat = function(pattern, opt_currency, opt_currencyStyle) { |
| this.intlCurrencyCode_ = opt_currency || |
| goog.i18n.NumberFormatSymbols.DEF_CURRENCY_CODE; |
| |
| this.currencyStyle_ = opt_currencyStyle || |
| goog.i18n.NumberFormat.CurrencyStyle.LOCAL; |
| |
| this.maximumIntegerDigits_ = 40; |
| this.minimumIntegerDigits_ = 1; |
| this.significantDigits_ = 0; // invariant, <= maximumFractionDigits |
| this.maximumFractionDigits_ = 3; // invariant, >= minFractionDigits |
| this.minimumFractionDigits_ = 0; |
| this.minExponentDigits_ = 0; |
| this.useSignForPositiveExponent_ = false; |
| |
| /** |
| * Whether to show trailing zeros in the fraction when significantDigits_ is |
| * positive. |
| * @private {boolean} |
| */ |
| this.showTrailingZeros_ = false; |
| |
| this.positivePrefix_ = ''; |
| this.positiveSuffix_ = ''; |
| this.negativePrefix_ = '-'; |
| this.negativeSuffix_ = ''; |
| |
| // The multiplier for use in percent, per mille, etc. |
| this.multiplier_ = 1; |
| this.groupingSize_ = 3; |
| this.decimalSeparatorAlwaysShown_ = false; |
| this.useExponentialNotation_ = false; |
| this.compactStyle_ = goog.i18n.NumberFormat.CompactStyle.NONE; |
| |
| /** |
| * The number to base the formatting on when using compact styles, or null |
| * if formatting should not be based on another number. |
| * @type {?number} |
| * @private |
| */ |
| this.baseFormattingNumber_ = null; |
| |
| /** @private {string} */ |
| this.pattern_; |
| |
| if (typeof pattern == 'number') { |
| this.applyStandardPattern_(pattern); |
| } else { |
| this.applyPattern_(pattern); |
| } |
| }; |
| |
| |
| /** |
| * Standard number formatting patterns. |
| * @enum {number} |
| */ |
| goog.i18n.NumberFormat.Format = { |
| DECIMAL: 1, |
| SCIENTIFIC: 2, |
| PERCENT: 3, |
| CURRENCY: 4, |
| COMPACT_SHORT: 5, |
| COMPACT_LONG: 6 |
| }; |
| |
| |
| /** |
| * Currency styles. |
| * @enum {number} |
| */ |
| goog.i18n.NumberFormat.CurrencyStyle = { |
| LOCAL: 0, // currency style as it is used in its circulating country. |
| PORTABLE: 1, // currency style that differentiate it from other popular ones. |
| GLOBAL: 2 // currency style that is unique among all currencies. |
| }; |
| |
| |
| /** |
| * Compacting styles. |
| * @enum {number} |
| */ |
| goog.i18n.NumberFormat.CompactStyle = { |
| NONE: 0, // Don't compact. |
| SHORT: 1, // Short compact form, such as 1.2B. |
| LONG: 2 // Long compact form, such as 1.2 billion. |
| }; |
| |
| |
| /** |
| * If the usage of Ascii digits should be enforced. |
| * @type {boolean} |
| * @private |
| */ |
| goog.i18n.NumberFormat.enforceAsciiDigits_ = false; |
| |
| |
| /** |
| * Set if the usage of Ascii digits in formatting should be enforced. |
| * @param {boolean} doEnforce Boolean value about if Ascii digits should be |
| * enforced. |
| */ |
| goog.i18n.NumberFormat.setEnforceAsciiDigits = function(doEnforce) { |
| goog.i18n.NumberFormat.enforceAsciiDigits_ = doEnforce; |
| }; |
| |
| |
| /** |
| * Return if Ascii digits is enforced. |
| * @return {boolean} If Ascii digits is enforced. |
| */ |
| goog.i18n.NumberFormat.isEnforceAsciiDigits = function() { |
| return goog.i18n.NumberFormat.enforceAsciiDigits_; |
| }; |
| |
| |
| /** |
| * Sets minimum number of fraction digits. |
| * @param {number} min the minimum. |
| * @return {!goog.i18n.NumberFormat} Reference to this NumberFormat object. |
| */ |
| goog.i18n.NumberFormat.prototype.setMinimumFractionDigits = function(min) { |
| if (this.significantDigits_ > 0 && min > 0) { |
| throw Error( |
| 'Can\'t combine significant digits and minimum fraction digits'); |
| } |
| this.minimumFractionDigits_ = min; |
| return this; |
| }; |
| |
| |
| /** |
| * Sets maximum number of fraction digits. |
| * @param {number} max the maximum. |
| * @return {!goog.i18n.NumberFormat} Reference to this NumberFormat object. |
| */ |
| goog.i18n.NumberFormat.prototype.setMaximumFractionDigits = function(max) { |
| this.maximumFractionDigits_ = max; |
| return this; |
| }; |
| |
| |
| /** |
| * Sets number of significant digits to show. Only fractions will be rounded. |
| * @param {number} number The number of significant digits to include. |
| * @return {!goog.i18n.NumberFormat} Reference to this NumberFormat object. |
| */ |
| goog.i18n.NumberFormat.prototype.setSignificantDigits = function(number) { |
| if (this.minimumFractionDigits_ > 0 && number >= 0) { |
| throw Error( |
| 'Can\'t combine significant digits and minimum fraction digits'); |
| } |
| this.significantDigits_ = number; |
| return this; |
| }; |
| |
| |
| /** |
| * Gets number of significant digits to show. Only fractions will be rounded. |
| * @return {number} The number of significant digits to include. |
| */ |
| goog.i18n.NumberFormat.prototype.getSignificantDigits = function() { |
| return this.significantDigits_; |
| }; |
| |
| |
| /** |
| * Sets whether trailing fraction zeros should be shown when significantDigits_ |
| * is positive. If this is true and significantDigits_ is 2, 1 will be formatted |
| * as '1.0'. |
| * @param {boolean} showTrailingZeros Whether trailing zeros should be shown. |
| * @return {!goog.i18n.NumberFormat} Reference to this NumberFormat object. |
| */ |
| goog.i18n.NumberFormat.prototype.setShowTrailingZeros = |
| function(showTrailingZeros) { |
| this.showTrailingZeros_ = showTrailingZeros; |
| return this; |
| }; |
| |
| |
| /** |
| * Sets a number to base the formatting on when compact style formatting is |
| * used. If this is null, the formatting should be based only on the number to |
| * be formatting. |
| * |
| * This base formatting number can be used to format the target number as |
| * another number would be formatted. For example, 100,000 is normally formatted |
| * as "100K" in the COMPACT_SHORT format. To instead format it as '0.1M', the |
| * base number could be set to 1,000,000 in order to force all numbers to be |
| * formatted in millions. Similarly, 1,000,000,000 would normally be formatted |
| * as '1B' and setting the base formatting number to 1,000,000, would cause it |
| * to be formatted instead as '1,000M'. |
| * |
| * @param {?number} baseFormattingNumber The number to base formatting on, or |
| * null if formatting should not be based on another number. |
| * @return {!goog.i18n.NumberFormat} Reference to this NumberFormat object. |
| */ |
| goog.i18n.NumberFormat.prototype.setBaseFormatting = |
| function(baseFormattingNumber) { |
| goog.asserts.assert(goog.isNull(baseFormattingNumber) || |
| isFinite(baseFormattingNumber)); |
| this.baseFormattingNumber_ = baseFormattingNumber; |
| return this; |
| }; |
| |
| |
| /** |
| * Gets the number on which compact formatting is currently based, or null if |
| * no such number is set. See setBaseFormatting() for more information. |
| * @return {?number} |
| */ |
| goog.i18n.NumberFormat.prototype.getBaseFormatting = function() { |
| return this.baseFormattingNumber_; |
| }; |
| |
| |
| /** |
| * Apply provided pattern, result are stored in member variables. |
| * |
| * @param {string} pattern String pattern being applied. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.applyPattern_ = function(pattern) { |
| this.pattern_ = pattern.replace(/ /g, '\u00a0'); |
| var pos = [0]; |
| |
| this.positivePrefix_ = this.parseAffix_(pattern, pos); |
| var trunkStart = pos[0]; |
| this.parseTrunk_(pattern, pos); |
| var trunkLen = pos[0] - trunkStart; |
| this.positiveSuffix_ = this.parseAffix_(pattern, pos); |
| if (pos[0] < pattern.length && |
| pattern.charAt(pos[0]) == goog.i18n.NumberFormat.PATTERN_SEPARATOR_) { |
| pos[0]++; |
| this.negativePrefix_ = this.parseAffix_(pattern, pos); |
| // we assume this part is identical to positive part. |
| // user must make sure the pattern is correctly constructed. |
| pos[0] += trunkLen; |
| this.negativeSuffix_ = this.parseAffix_(pattern, pos); |
| } else { |
| // if no negative affix specified, they share the same positive affix |
| this.negativePrefix_ = this.positivePrefix_ + this.negativePrefix_; |
| this.negativeSuffix_ += this.positiveSuffix_; |
| } |
| }; |
| |
| |
| /** |
| * Apply a predefined pattern to NumberFormat object. |
| * @param {number} patternType The number that indicates a predefined number |
| * format pattern. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.applyStandardPattern_ = function(patternType) { |
| switch (patternType) { |
| case goog.i18n.NumberFormat.Format.DECIMAL: |
| this.applyPattern_(goog.i18n.NumberFormatSymbols.DECIMAL_PATTERN); |
| break; |
| case goog.i18n.NumberFormat.Format.SCIENTIFIC: |
| this.applyPattern_(goog.i18n.NumberFormatSymbols.SCIENTIFIC_PATTERN); |
| break; |
| case goog.i18n.NumberFormat.Format.PERCENT: |
| this.applyPattern_(goog.i18n.NumberFormatSymbols.PERCENT_PATTERN); |
| break; |
| case goog.i18n.NumberFormat.Format.CURRENCY: |
| this.applyPattern_(goog.i18n.currency.adjustPrecision( |
| goog.i18n.NumberFormatSymbols.CURRENCY_PATTERN, |
| this.intlCurrencyCode_)); |
| break; |
| case goog.i18n.NumberFormat.Format.COMPACT_SHORT: |
| this.applyCompactStyle_(goog.i18n.NumberFormat.CompactStyle.SHORT); |
| break; |
| case goog.i18n.NumberFormat.Format.COMPACT_LONG: |
| this.applyCompactStyle_(goog.i18n.NumberFormat.CompactStyle.LONG); |
| break; |
| default: |
| throw Error('Unsupported pattern type.'); |
| } |
| }; |
| |
| |
| /** |
| * Apply a predefined pattern for shorthand formats. |
| * @param {goog.i18n.NumberFormat.CompactStyle} style the compact style to |
| * set defaults for. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.applyCompactStyle_ = function(style) { |
| this.compactStyle_ = style; |
| this.applyPattern_(goog.i18n.NumberFormatSymbols.DECIMAL_PATTERN); |
| this.setMinimumFractionDigits(0); |
| this.setMaximumFractionDigits(2); |
| this.setSignificantDigits(2); |
| }; |
| |
| |
| /** |
| * Parses text string to produce a Number. |
| * |
| * This method attempts to parse text starting from position "opt_pos" if it |
| * is given. Otherwise the parse will start from the beginning of the text. |
| * When opt_pos presents, opt_pos will be updated to the character next to where |
| * parsing stops after the call. If an error occurs, opt_pos won't be updated. |
| * |
| * @param {string} text The string to be parsed. |
| * @param {Array<number>=} opt_pos Position to pass in and get back. |
| * @return {number} Parsed number. This throws an error if the text cannot be |
| * parsed. |
| */ |
| goog.i18n.NumberFormat.prototype.parse = function(text, opt_pos) { |
| var pos = opt_pos || [0]; |
| |
| if (this.compactStyle_ != goog.i18n.NumberFormat.CompactStyle.NONE) { |
| throw Error('Parsing of compact numbers is unimplemented'); |
| } |
| |
| var ret = NaN; |
| |
| // we don't want to handle 2 kind of space in parsing, normalize it to nbsp |
| text = text.replace(/ /g, '\u00a0'); |
| |
| var gotPositive = text.indexOf(this.positivePrefix_, pos[0]) == pos[0]; |
| var gotNegative = text.indexOf(this.negativePrefix_, pos[0]) == pos[0]; |
| |
| // check for the longest match |
| if (gotPositive && gotNegative) { |
| if (this.positivePrefix_.length > this.negativePrefix_.length) { |
| gotNegative = false; |
| } else if (this.positivePrefix_.length < this.negativePrefix_.length) { |
| gotPositive = false; |
| } |
| } |
| |
| if (gotPositive) { |
| pos[0] += this.positivePrefix_.length; |
| } else if (gotNegative) { |
| pos[0] += this.negativePrefix_.length; |
| } |
| |
| // process digits or Inf, find decimal position |
| if (text.indexOf(goog.i18n.NumberFormatSymbols.INFINITY, pos[0]) == pos[0]) { |
| pos[0] += goog.i18n.NumberFormatSymbols.INFINITY.length; |
| ret = Infinity; |
| } else { |
| ret = this.parseNumber_(text, pos); |
| } |
| |
| // check for suffix |
| if (gotPositive) { |
| if (!(text.indexOf(this.positiveSuffix_, pos[0]) == pos[0])) { |
| return NaN; |
| } |
| pos[0] += this.positiveSuffix_.length; |
| } else if (gotNegative) { |
| if (!(text.indexOf(this.negativeSuffix_, pos[0]) == pos[0])) { |
| return NaN; |
| } |
| pos[0] += this.negativeSuffix_.length; |
| } |
| |
| return gotNegative ? -ret : ret; |
| }; |
| |
| |
| /** |
| * This function will parse a "localized" text into a Number. It needs to |
| * handle locale specific decimal, grouping, exponent and digits. |
| * |
| * @param {string} text The text that need to be parsed. |
| * @param {Array<number>} pos In/out parsing position. In case of failure, |
| * pos value won't be changed. |
| * @return {number} Number value, or NaN if nothing can be parsed. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.parseNumber_ = function(text, pos) { |
| var sawDecimal = false; |
| var sawExponent = false; |
| var sawDigit = false; |
| var scale = 1; |
| var decimal = goog.i18n.NumberFormatSymbols.DECIMAL_SEP; |
| var grouping = goog.i18n.NumberFormatSymbols.GROUP_SEP; |
| var exponentChar = goog.i18n.NumberFormatSymbols.EXP_SYMBOL; |
| |
| if (this.compactStyle_ != goog.i18n.NumberFormat.CompactStyle.NONE) { |
| throw Error('Parsing of compact style numbers is not implemented'); |
| } |
| |
| var normalizedText = ''; |
| for (; pos[0] < text.length; pos[0]++) { |
| var ch = text.charAt(pos[0]); |
| var digit = this.getDigit_(ch); |
| if (digit >= 0 && digit <= 9) { |
| normalizedText += digit; |
| sawDigit = true; |
| } else if (ch == decimal.charAt(0)) { |
| if (sawDecimal || sawExponent) { |
| break; |
| } |
| normalizedText += '.'; |
| sawDecimal = true; |
| } else if (ch == grouping.charAt(0) && |
| ('\u00a0' != grouping.charAt(0) || |
| pos[0] + 1 < text.length && |
| this.getDigit_(text.charAt(pos[0] + 1)) >= 0)) { |
| // Got a grouping character here. When grouping character is nbsp, need |
| // to make sure the character following it is a digit. |
| if (sawDecimal || sawExponent) { |
| break; |
| } |
| continue; |
| } else if (ch == exponentChar.charAt(0)) { |
| if (sawExponent) { |
| break; |
| } |
| normalizedText += 'E'; |
| sawExponent = true; |
| } else if (ch == '+' || ch == '-') { |
| normalizedText += ch; |
| } else if (ch == goog.i18n.NumberFormatSymbols.PERCENT.charAt(0)) { |
| if (scale != 1) { |
| break; |
| } |
| scale = 100; |
| if (sawDigit) { |
| pos[0]++; // eat this character if parse end here |
| break; |
| } |
| } else if (ch == goog.i18n.NumberFormatSymbols.PERMILL.charAt(0)) { |
| if (scale != 1) { |
| break; |
| } |
| scale = 1000; |
| if (sawDigit) { |
| pos[0]++; // eat this character if parse end here |
| break; |
| } |
| } else { |
| break; |
| } |
| } |
| return parseFloat(normalizedText) / scale; |
| }; |
| |
| |
| /** |
| * Formats a Number to produce a string. |
| * |
| * @param {number} number The Number to be formatted. |
| * @return {string} The formatted number string. |
| */ |
| goog.i18n.NumberFormat.prototype.format = function(number) { |
| if (isNaN(number)) { |
| return goog.i18n.NumberFormatSymbols.NAN; |
| } |
| |
| var parts = []; |
| var baseFormattingNumber = goog.isNull(this.baseFormattingNumber_) ? |
| number : |
| this.baseFormattingNumber_; |
| var unit = this.getUnitAfterRounding_(baseFormattingNumber, number); |
| number /= Math.pow(10, unit.divisorBase); |
| |
| parts.push(unit.prefix); |
| |
| // in icu code, it is commented that certain computation need to keep the |
| // negative sign for 0. |
| var isNegative = number < 0.0 || number == 0.0 && 1 / number < 0.0; |
| |
| parts.push(isNegative ? this.negativePrefix_ : this.positivePrefix_); |
| |
| if (!isFinite(number)) { |
| parts.push(goog.i18n.NumberFormatSymbols.INFINITY); |
| } else { |
| // convert number to non-negative value |
| number *= isNegative ? -1 : 1; |
| |
| number *= this.multiplier_; |
| this.useExponentialNotation_ ? |
| this.subformatExponential_(number, parts) : |
| this.subformatFixed_(number, this.minimumIntegerDigits_, parts); |
| } |
| |
| parts.push(isNegative ? this.negativeSuffix_ : this.positiveSuffix_); |
| parts.push(unit.suffix); |
| |
| return parts.join(''); |
| }; |
| |
| |
| /** |
| * Round a number into an integer and fractional part |
| * based on the rounding rules for this NumberFormat. |
| * @param {number} number The number to round. |
| * @return {{intValue: number, fracValue: number}} The integer and fractional |
| * part after rounding. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.roundNumber_ = function(number) { |
| var power = Math.pow(10, this.maximumFractionDigits_); |
| var shiftedNumber = this.significantDigits_ <= 0 ? |
| Math.round(number * power) : |
| Math.round(this.roundToSignificantDigits_( |
| number * power, |
| this.significantDigits_, |
| this.maximumFractionDigits_)); |
| |
| var intValue, fracValue; |
| if (isFinite(shiftedNumber)) { |
| intValue = Math.floor(shiftedNumber / power); |
| fracValue = Math.floor(shiftedNumber - intValue * power); |
| } else { |
| intValue = number; |
| fracValue = 0; |
| } |
| return {intValue: intValue, fracValue: fracValue}; |
| }; |
| |
| |
| /** |
| * Formats a Number in fraction format. |
| * |
| * @param {number} number |
| * @param {number} minIntDigits Minimum integer digits. |
| * @param {Array<string>} parts |
| * This array holds the pieces of formatted string. |
| * This function will add its formatted pieces to the array. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.subformatFixed_ = |
| function(number, minIntDigits, parts) { |
| if (this.minimumFractionDigits_ > this.maximumFractionDigits_) { |
| throw Error('Min value must be less than max value'); |
| } |
| |
| var rounded = this.roundNumber_(number); |
| var power = Math.pow(10, this.maximumFractionDigits_); |
| var intValue = rounded.intValue; |
| var fracValue = rounded.fracValue; |
| |
| var numIntDigits = (intValue == 0) ? 0 : this.intLog10_(intValue) + 1; |
| var fractionPresent = this.minimumFractionDigits_ > 0 || fracValue > 0 || |
| (this.showTrailingZeros_ && numIntDigits < this.significantDigits_); |
| var minimumFractionDigits = this.minimumFractionDigits_; |
| if (fractionPresent) { |
| if (this.showTrailingZeros_ && this.significantDigits_ > 0) { |
| minimumFractionDigits = this.significantDigits_ - numIntDigits; |
| } else { |
| minimumFractionDigits = this.minimumFractionDigits_; |
| } |
| } |
| |
| var intPart = ''; |
| var translatableInt = intValue; |
| while (translatableInt > 1E20) { |
| // here it goes beyond double precision, add '0' make it look better |
| intPart = '0' + intPart; |
| translatableInt = Math.round(translatableInt / 10); |
| } |
| intPart = translatableInt + intPart; |
| |
| var decimal = goog.i18n.NumberFormatSymbols.DECIMAL_SEP; |
| var grouping = goog.i18n.NumberFormatSymbols.GROUP_SEP; |
| var zeroCode = goog.i18n.NumberFormat.enforceAsciiDigits_ ? |
| 48 /* ascii '0' */ : |
| goog.i18n.NumberFormatSymbols.ZERO_DIGIT.charCodeAt(0); |
| var digitLen = intPart.length; |
| |
| if (intValue > 0 || minIntDigits > 0) { |
| for (var i = digitLen; i < minIntDigits; i++) { |
| parts.push(String.fromCharCode(zeroCode)); |
| } |
| |
| for (var i = 0; i < digitLen; i++) { |
| parts.push(String.fromCharCode(zeroCode + intPart.charAt(i) * 1)); |
| |
| if (digitLen - i > 1 && this.groupingSize_ > 0 && |
| ((digitLen - i) % this.groupingSize_ == 1)) { |
| parts.push(grouping); |
| } |
| } |
| } else if (!fractionPresent) { |
| // If there is no fraction present, and we haven't printed any |
| // integer digits, then print a zero. |
| parts.push(String.fromCharCode(zeroCode)); |
| } |
| |
| // Output the decimal separator if we always do so. |
| if (this.decimalSeparatorAlwaysShown_ || fractionPresent) { |
| parts.push(decimal); |
| } |
| |
| var fracPart = '' + (fracValue + power); |
| var fracLen = fracPart.length; |
| while (fracPart.charAt(fracLen - 1) == '0' && |
| fracLen > minimumFractionDigits + 1) { |
| fracLen--; |
| } |
| |
| for (var i = 1; i < fracLen; i++) { |
| parts.push(String.fromCharCode(zeroCode + fracPart.charAt(i) * 1)); |
| } |
| }; |
| |
| |
| /** |
| * Formats exponent part of a Number. |
| * |
| * @param {number} exponent Exponential value. |
| * @param {Array<string>} parts The array that holds the pieces of formatted |
| * string. This function will append more formatted pieces to the array. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.addExponentPart_ = function(exponent, parts) { |
| parts.push(goog.i18n.NumberFormatSymbols.EXP_SYMBOL); |
| |
| if (exponent < 0) { |
| exponent = -exponent; |
| parts.push(goog.i18n.NumberFormatSymbols.MINUS_SIGN); |
| } else if (this.useSignForPositiveExponent_) { |
| parts.push(goog.i18n.NumberFormatSymbols.PLUS_SIGN); |
| } |
| |
| var exponentDigits = '' + exponent; |
| var zeroChar = goog.i18n.NumberFormat.enforceAsciiDigits_ ? '0' : |
| goog.i18n.NumberFormatSymbols.ZERO_DIGIT; |
| for (var i = exponentDigits.length; i < this.minExponentDigits_; i++) { |
| parts.push(zeroChar); |
| } |
| parts.push(exponentDigits); |
| }; |
| |
| |
| /** |
| * Formats Number in exponential format. |
| * |
| * @param {number} number Value need to be formated. |
| * @param {Array<string>} parts The array that holds the pieces of formatted |
| * string. This function will append more formatted pieces to the array. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.subformatExponential_ = |
| function(number, parts) { |
| if (number == 0.0) { |
| this.subformatFixed_(number, this.minimumIntegerDigits_, parts); |
| this.addExponentPart_(0, parts); |
| return; |
| } |
| |
| var exponent = goog.math.safeFloor(Math.log(number) / Math.log(10)); |
| number /= Math.pow(10, exponent); |
| |
| var minIntDigits = this.minimumIntegerDigits_; |
| if (this.maximumIntegerDigits_ > 1 && |
| this.maximumIntegerDigits_ > this.minimumIntegerDigits_) { |
| // A repeating range is defined; adjust to it as follows. |
| // If repeat == 3, we have 6,5,4=>3; 3,2,1=>0; 0,-1,-2=>-3; |
| // -3,-4,-5=>-6, etc. This takes into account that the |
| // exponent we have here is off by one from what we expect; |
| // it is for the format 0.MMMMMx10^n. |
| while ((exponent % this.maximumIntegerDigits_) != 0) { |
| number *= 10; |
| exponent--; |
| } |
| minIntDigits = 1; |
| } else { |
| // No repeating range is defined; use minimum integer digits. |
| if (this.minimumIntegerDigits_ < 1) { |
| exponent++; |
| number /= 10; |
| } else { |
| exponent -= this.minimumIntegerDigits_ - 1; |
| number *= Math.pow(10, this.minimumIntegerDigits_ - 1); |
| } |
| } |
| this.subformatFixed_(number, minIntDigits, parts); |
| this.addExponentPart_(exponent, parts); |
| }; |
| |
| |
| /** |
| * Returns the digit value of current character. The character could be either |
| * '0' to '9', or a locale specific digit. |
| * |
| * @param {string} ch Character that represents a digit. |
| * @return {number} The digit value, or -1 on error. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.getDigit_ = function(ch) { |
| var code = ch.charCodeAt(0); |
| // between '0' to '9' |
| if (48 <= code && code < 58) { |
| return code - 48; |
| } else { |
| var zeroCode = goog.i18n.NumberFormatSymbols.ZERO_DIGIT.charCodeAt(0); |
| return zeroCode <= code && code < zeroCode + 10 ? code - zeroCode : -1; |
| } |
| }; |
| |
| |
| // ---------------------------------------------------------------------- |
| // CONSTANTS |
| // ---------------------------------------------------------------------- |
| // Constants for characters used in programmatic (unlocalized) patterns. |
| /** |
| * A zero digit character. |
| * @type {string} |
| * @private |
| */ |
| goog.i18n.NumberFormat.PATTERN_ZERO_DIGIT_ = '0'; |
| |
| |
| /** |
| * A grouping separator character. |
| * @type {string} |
| * @private |
| */ |
| goog.i18n.NumberFormat.PATTERN_GROUPING_SEPARATOR_ = ','; |
| |
| |
| /** |
| * A decimal separator character. |
| * @type {string} |
| * @private |
| */ |
| goog.i18n.NumberFormat.PATTERN_DECIMAL_SEPARATOR_ = '.'; |
| |
| |
| /** |
| * A per mille character. |
| * @type {string} |
| * @private |
| */ |
| goog.i18n.NumberFormat.PATTERN_PER_MILLE_ = '\u2030'; |
| |
| |
| /** |
| * A percent character. |
| * @type {string} |
| * @private |
| */ |
| goog.i18n.NumberFormat.PATTERN_PERCENT_ = '%'; |
| |
| |
| /** |
| * A digit character. |
| * @type {string} |
| * @private |
| */ |
| goog.i18n.NumberFormat.PATTERN_DIGIT_ = '#'; |
| |
| |
| /** |
| * A separator character. |
| * @type {string} |
| * @private |
| */ |
| goog.i18n.NumberFormat.PATTERN_SEPARATOR_ = ';'; |
| |
| |
| /** |
| * An exponent character. |
| * @type {string} |
| * @private |
| */ |
| goog.i18n.NumberFormat.PATTERN_EXPONENT_ = 'E'; |
| |
| |
| /** |
| * An plus character. |
| * @type {string} |
| * @private |
| */ |
| goog.i18n.NumberFormat.PATTERN_PLUS_ = '+'; |
| |
| |
| /** |
| * A minus character. |
| * @type {string} |
| * @private |
| */ |
| goog.i18n.NumberFormat.PATTERN_MINUS_ = '-'; |
| |
| |
| /** |
| * A quote character. |
| * @type {string} |
| * @private |
| */ |
| goog.i18n.NumberFormat.PATTERN_CURRENCY_SIGN_ = '\u00A4'; |
| |
| |
| /** |
| * A quote character. |
| * @type {string} |
| * @private |
| */ |
| goog.i18n.NumberFormat.QUOTE_ = '\''; |
| |
| |
| /** |
| * Parses affix part of pattern. |
| * |
| * @param {string} pattern Pattern string that need to be parsed. |
| * @param {Array<number>} pos One element position array to set and receive |
| * parsing position. |
| * |
| * @return {string} Affix received from parsing. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.parseAffix_ = function(pattern, pos) { |
| var affix = ''; |
| var inQuote = false; |
| var len = pattern.length; |
| |
| for (; pos[0] < len; pos[0]++) { |
| var ch = pattern.charAt(pos[0]); |
| if (ch == goog.i18n.NumberFormat.QUOTE_) { |
| if (pos[0] + 1 < len && |
| pattern.charAt(pos[0] + 1) == goog.i18n.NumberFormat.QUOTE_) { |
| pos[0]++; |
| affix += '\''; // 'don''t' |
| } else { |
| inQuote = !inQuote; |
| } |
| continue; |
| } |
| |
| if (inQuote) { |
| affix += ch; |
| } else { |
| switch (ch) { |
| case goog.i18n.NumberFormat.PATTERN_DIGIT_: |
| case goog.i18n.NumberFormat.PATTERN_ZERO_DIGIT_: |
| case goog.i18n.NumberFormat.PATTERN_GROUPING_SEPARATOR_: |
| case goog.i18n.NumberFormat.PATTERN_DECIMAL_SEPARATOR_: |
| case goog.i18n.NumberFormat.PATTERN_SEPARATOR_: |
| return affix; |
| case goog.i18n.NumberFormat.PATTERN_CURRENCY_SIGN_: |
| if ((pos[0] + 1) < len && |
| pattern.charAt(pos[0] + 1) == |
| goog.i18n.NumberFormat.PATTERN_CURRENCY_SIGN_) { |
| pos[0]++; |
| affix += this.intlCurrencyCode_; |
| } else { |
| switch (this.currencyStyle_) { |
| case goog.i18n.NumberFormat.CurrencyStyle.LOCAL: |
| affix += goog.i18n.currency.getLocalCurrencySign( |
| this.intlCurrencyCode_); |
| break; |
| case goog.i18n.NumberFormat.CurrencyStyle.GLOBAL: |
| affix += goog.i18n.currency.getGlobalCurrencySign( |
| this.intlCurrencyCode_); |
| break; |
| case goog.i18n.NumberFormat.CurrencyStyle.PORTABLE: |
| affix += goog.i18n.currency.getPortableCurrencySign( |
| this.intlCurrencyCode_); |
| break; |
| default: |
| break; |
| } |
| } |
| break; |
| case goog.i18n.NumberFormat.PATTERN_PERCENT_: |
| if (this.multiplier_ != 1) { |
| throw Error('Too many percent/permill'); |
| } |
| this.multiplier_ = 100; |
| affix += goog.i18n.NumberFormatSymbols.PERCENT; |
| break; |
| case goog.i18n.NumberFormat.PATTERN_PER_MILLE_: |
| if (this.multiplier_ != 1) { |
| throw Error('Too many percent/permill'); |
| } |
| this.multiplier_ = 1000; |
| affix += goog.i18n.NumberFormatSymbols.PERMILL; |
| break; |
| default: |
| affix += ch; |
| } |
| } |
| } |
| |
| return affix; |
| }; |
| |
| |
| /** |
| * Parses the trunk part of a pattern. |
| * |
| * @param {string} pattern Pattern string that need to be parsed. |
| * @param {Array<number>} pos One element position array to set and receive |
| * parsing position. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.parseTrunk_ = function(pattern, pos) { |
| var decimalPos = -1; |
| var digitLeftCount = 0; |
| var zeroDigitCount = 0; |
| var digitRightCount = 0; |
| var groupingCount = -1; |
| |
| var len = pattern.length; |
| for (var loop = true; pos[0] < len && loop; pos[0]++) { |
| var ch = pattern.charAt(pos[0]); |
| switch (ch) { |
| case goog.i18n.NumberFormat.PATTERN_DIGIT_: |
| if (zeroDigitCount > 0) { |
| digitRightCount++; |
| } else { |
| digitLeftCount++; |
| } |
| if (groupingCount >= 0 && decimalPos < 0) { |
| groupingCount++; |
| } |
| break; |
| case goog.i18n.NumberFormat.PATTERN_ZERO_DIGIT_: |
| if (digitRightCount > 0) { |
| throw Error('Unexpected "0" in pattern "' + pattern + '"'); |
| } |
| zeroDigitCount++; |
| if (groupingCount >= 0 && decimalPos < 0) { |
| groupingCount++; |
| } |
| break; |
| case goog.i18n.NumberFormat.PATTERN_GROUPING_SEPARATOR_: |
| groupingCount = 0; |
| break; |
| case goog.i18n.NumberFormat.PATTERN_DECIMAL_SEPARATOR_: |
| if (decimalPos >= 0) { |
| throw Error('Multiple decimal separators in pattern "' + |
| pattern + '"'); |
| } |
| decimalPos = digitLeftCount + zeroDigitCount + digitRightCount; |
| break; |
| case goog.i18n.NumberFormat.PATTERN_EXPONENT_: |
| if (this.useExponentialNotation_) { |
| throw Error('Multiple exponential symbols in pattern "' + |
| pattern + '"'); |
| } |
| this.useExponentialNotation_ = true; |
| this.minExponentDigits_ = 0; |
| |
| // exponent pattern can have a optional '+'. |
| if ((pos[0] + 1) < len && pattern.charAt(pos[0] + 1) == |
| goog.i18n.NumberFormat.PATTERN_PLUS_) { |
| pos[0]++; |
| this.useSignForPositiveExponent_ = true; |
| } |
| |
| // Use lookahead to parse out the exponential part |
| // of the pattern, then jump into phase 2. |
| while ((pos[0] + 1) < len && pattern.charAt(pos[0] + 1) == |
| goog.i18n.NumberFormat.PATTERN_ZERO_DIGIT_) { |
| pos[0]++; |
| this.minExponentDigits_++; |
| } |
| |
| if ((digitLeftCount + zeroDigitCount) < 1 || |
| this.minExponentDigits_ < 1) { |
| throw Error('Malformed exponential pattern "' + pattern + '"'); |
| } |
| loop = false; |
| break; |
| default: |
| pos[0]--; |
| loop = false; |
| break; |
| } |
| } |
| |
| if (zeroDigitCount == 0 && digitLeftCount > 0 && decimalPos >= 0) { |
| // Handle '###.###' and '###.' and '.###' |
| var n = decimalPos; |
| if (n == 0) { // Handle '.###' |
| n++; |
| } |
| digitRightCount = digitLeftCount - n; |
| digitLeftCount = n - 1; |
| zeroDigitCount = 1; |
| } |
| |
| // Do syntax checking on the digits. |
| if (decimalPos < 0 && digitRightCount > 0 || |
| decimalPos >= 0 && (decimalPos < digitLeftCount || |
| decimalPos > digitLeftCount + zeroDigitCount) || |
| groupingCount == 0) { |
| throw Error('Malformed pattern "' + pattern + '"'); |
| } |
| var totalDigits = digitLeftCount + zeroDigitCount + digitRightCount; |
| |
| this.maximumFractionDigits_ = decimalPos >= 0 ? totalDigits - decimalPos : 0; |
| if (decimalPos >= 0) { |
| this.minimumFractionDigits_ = digitLeftCount + zeroDigitCount - decimalPos; |
| if (this.minimumFractionDigits_ < 0) { |
| this.minimumFractionDigits_ = 0; |
| } |
| } |
| |
| // The effectiveDecimalPos is the position the decimal is at or would be at |
| // if there is no decimal. Note that if decimalPos<0, then digitTotalCount == |
| // digitLeftCount + zeroDigitCount. |
| var effectiveDecimalPos = decimalPos >= 0 ? decimalPos : totalDigits; |
| this.minimumIntegerDigits_ = effectiveDecimalPos - digitLeftCount; |
| if (this.useExponentialNotation_) { |
| this.maximumIntegerDigits_ = digitLeftCount + this.minimumIntegerDigits_; |
| |
| // in exponential display, we need to at least show something. |
| if (this.maximumFractionDigits_ == 0 && this.minimumIntegerDigits_ == 0) { |
| this.minimumIntegerDigits_ = 1; |
| } |
| } |
| |
| this.groupingSize_ = Math.max(0, groupingCount); |
| this.decimalSeparatorAlwaysShown_ = decimalPos == 0 || |
| decimalPos == totalDigits; |
| }; |
| |
| |
| /** |
| * Alias for the compact format 'unit' object. |
| * @typedef {{ |
| * prefix: string, |
| * suffix: string, |
| * divisorBase: number |
| * }} |
| */ |
| goog.i18n.NumberFormat.CompactNumberUnit; |
| |
| |
| /** |
| * The empty unit, corresponding to a base of 0. |
| * @private {!goog.i18n.NumberFormat.CompactNumberUnit} |
| */ |
| goog.i18n.NumberFormat.NULL_UNIT_ = { prefix: '', suffix: '', divisorBase: 0 }; |
| |
| |
| /** |
| * Get compact unit for a certain number of digits |
| * |
| * @param {number} base The number of digits to get the unit for. |
| * @param {string} plurality The plurality of the number. |
| * @return {!goog.i18n.NumberFormat.CompactNumberUnit} The compact unit. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.getUnitFor_ = function(base, plurality) { |
| var table = this.compactStyle_ == goog.i18n.NumberFormat.CompactStyle.SHORT ? |
| goog.i18n.CompactNumberFormatSymbols.COMPACT_DECIMAL_SHORT_PATTERN : |
| goog.i18n.CompactNumberFormatSymbols.COMPACT_DECIMAL_LONG_PATTERN; |
| |
| if (base < 3) { |
| return goog.i18n.NumberFormat.NULL_UNIT_; |
| } else { |
| base = Math.min(14, base); |
| var patterns = table[Math.pow(10, base)]; |
| if (!patterns) { |
| return goog.i18n.NumberFormat.NULL_UNIT_; |
| } |
| |
| var pattern = patterns[plurality]; |
| if (!pattern || pattern == '0') { |
| return goog.i18n.NumberFormat.NULL_UNIT_; |
| } |
| |
| var parts = /([^0]*)(0+)(.*)/.exec(pattern); |
| if (!parts) { |
| return goog.i18n.NumberFormat.NULL_UNIT_; |
| } |
| |
| return { |
| prefix: parts[1], |
| suffix: parts[3], |
| divisorBase: base - (parts[2].length - 1) |
| }; |
| } |
| }; |
| |
| |
| /** |
| * Get the compact unit divisor, accounting for rounding of the quantity. |
| * |
| * @param {number} formattingNumber The number to base the formatting on. The |
| * unit will be calculated from this number. |
| * @param {number} pluralityNumber The number to use for calculating the |
| * plurality. |
| * @return {!goog.i18n.NumberFormat.CompactNumberUnit} The unit after rounding. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.getUnitAfterRounding_ = |
| function(formattingNumber, pluralityNumber) { |
| if (this.compactStyle_ == goog.i18n.NumberFormat.CompactStyle.NONE) { |
| return goog.i18n.NumberFormat.NULL_UNIT_; |
| } |
| |
| formattingNumber = Math.abs(formattingNumber); |
| pluralityNumber = Math.abs(pluralityNumber); |
| |
| var initialPlurality = this.pluralForm_(formattingNumber); |
| // Compute the exponent from the formattingNumber, to compute the unit. |
| var base = formattingNumber <= 1 ? 0 : this.intLog10_(formattingNumber); |
| var initialDivisor = this.getUnitFor_(base, initialPlurality).divisorBase; |
| // Round both numbers based on the unit used. |
| var pluralityAttempt = pluralityNumber / Math.pow(10, initialDivisor); |
| var pluralityRounded = this.roundNumber_(pluralityAttempt); |
| var formattingAttempt = formattingNumber / Math.pow(10, initialDivisor); |
| var formattingRounded = this.roundNumber_(formattingAttempt); |
| // Compute the plurality of the pluralityNumber when formatted using the name |
| // units as the formattingNumber. |
| var finalPlurality = |
| this.pluralForm_(pluralityRounded.intValue + pluralityRounded.fracValue); |
| // Get the final unit, using the rounded formatting number to get the correct |
| // unit, and the plurality computed from the pluralityNumber. |
| return this.getUnitFor_( |
| initialDivisor + this.intLog10_(formattingRounded.intValue), |
| finalPlurality); |
| }; |
| |
| |
| /** |
| * Get the integer base 10 logarithm of a number. |
| * |
| * @param {number} number The number to log. |
| * @return {number} The lowest integer n such that 10^n >= number. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.intLog10_ = function(number) { |
| // Turns out Math.log(1000000)/Math.LN10 is strictly less than 6. |
| var i = 0; |
| while ((number /= 10) >= 1) i++; |
| return i; |
| }; |
| |
| |
| /** |
| * Round to a certain number of significant digits. |
| * |
| * @param {number} number The number to round. |
| * @param {number} significantDigits The number of significant digits |
| * to round to. |
| * @param {number} scale Treat number as fixed point times 10^scale. |
| * @return {number} The rounded number. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.roundToSignificantDigits_ = |
| function(number, significantDigits, scale) { |
| if (!number) |
| return number; |
| |
| var digits = this.intLog10_(number); |
| var magnitude = significantDigits - digits - 1; |
| |
| // Only round fraction, not (potentially shifted) integers. |
| if (magnitude < -scale) { |
| var point = Math.pow(10, scale); |
| return Math.round(number / point) * point; |
| } |
| |
| var power = Math.pow(10, magnitude); |
| var shifted = Math.round(number * power); |
| return shifted / power; |
| }; |
| |
| |
| /** |
| * Get the plural form of a number. |
| * @param {number} quantity The quantity to find plurality of. |
| * @return {string} One of 'zero', 'one', 'two', 'few', 'many', 'other'. |
| * @private |
| */ |
| goog.i18n.NumberFormat.prototype.pluralForm_ = function(quantity) { |
| /* TODO: Implement */ |
| return 'other'; |
| }; |
| |
| |
| /** |
| * Checks if the currency symbol comes before the value ($12) or after (12$) |
| * Handy for applications that need to have separate UI fields for the currency |
| * value and symbol, especially for input: Price: [USD] [123.45] |
| * The currency symbol might be a combo box, or a label. |
| * |
| * @return {boolean} true if currency is before value. |
| */ |
| goog.i18n.NumberFormat.prototype.isCurrencyCodeBeforeValue = function() { |
| var posCurrSymbol = this.pattern_.indexOf('\u00A4'); // 'ยค' Currency sign |
| var posPound = this.pattern_.indexOf('#'); |
| var posZero = this.pattern_.indexOf('0'); |
| |
| // posCurrValue is the first '#' or '0' found. |
| // If none of them is found (not possible, but still), |
| // the result is true (postCurrSymbol < MAX_VALUE) |
| // That is OK, matches the en_US and ROOT locales. |
| var posCurrValue = Number.MAX_VALUE; |
| if (posPound >= 0 && posPound < posCurrValue) { |
| posCurrValue = posPound; |
| } |
| if (posZero >= 0 && posZero < posCurrValue) { |
| posCurrValue = posZero; |
| } |
| |
| // No need to test, it is guaranteed that both these symbols exist. |
| // If not, we have bigger problems than this. |
| return posCurrSymbol < posCurrValue; |
| }; |