UNPKG

sku-pg

Version:

Skulpt is a Javascript implementation of Python 2.x. Python that runs in your browser!

764 lines (665 loc) 22.8 kB
// 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.require('goog.i18n.NumberFormatSymbols'); goog.require('goog.i18n.currencyCodeMap'); /** * 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. * @constructor */ goog.i18n.NumberFormat = function(pattern, opt_currency) { this.intlCurrencyCode_ = opt_currency || goog.i18n.NumberFormatSymbols.DEF_CURRENCY_CODE; this.currencySymbol_ = goog.i18n.currencyCodeMap[this.intlCurrencyCode_]; this.maximumIntegerDigits_ = 40; this.minimumIntegerDigits_ = 1; this.maximumFractionDigits_ = 3; // invariant, >= minFractionDigits this.minimumFractionDigits_ = 0; this.minExponentDigits_ = 0; 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; 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 }; /** * 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.NumberFormatSymbols.CURRENCY_PATTERN); break; default: throw Error('Unsupported pattern type.'); } }; /** * 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]; var start = pos[0]; 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; 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 = []; // 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_); return parts.join(''); }; /** * Formats a Number in fraction format. * * @param {number} number Value need to be formated. * @param {number} minIntDigits Minimum integer digits. * @param {Array} 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) { // round the number var power = Math.pow(10, this.maximumFractionDigits_); number = Math.round(number * power); var intValue = Math.floor(number / power); var fracValue = Math.floor(number - intValue * power); var fractionPresent = this.minimumFractionDigits_ > 0 || fracValue > 0; 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.NumberFormatSymbols.ZERO_DIGIT.charCodeAt(0); var digitLen = intPart.length; if (intValue > 0 || minIntDigits > 0) { for (var i = digitLen; i < minIntDigits; i++) { parts.push(goog.i18n.NumberFormatSymbols.ZERO_DIGIT); } 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(goog.i18n.NumberFormatSymbols.ZERO_DIGIT); } // 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 > this.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); } var exponentDigits = '' + exponent; for (var i = exponentDigits.length; i < this.minExponentDigits_; i++) { parts.push(goog.i18n.NumberFormatSymbols.ZERO_DIGIT); } 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 = Math.floor(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'; /** * 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 { affix += this.currencySymbol_; } 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; // 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; };