chromium/third_party/google-closure-library/closure/goog/i18n/numberformat.js

/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @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.NumberFormatSymbolsType');
goog.require('goog.i18n.NumberFormatSymbols_u_nu_latn');
goog.require('goog.i18n.currency');
goog.require('goog.i18n.currency.CurrencyInfo');
goog.require('goog.math');
goog.require('goog.string');

/**
 * 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 the current locale will be used.
 * @param {number=} opt_currencyStyle currency style, value defined in
 *     goog.i18n.NumberFormat.CurrencyStyle. If not given, the currency style
 *     for the current locale will be used.
 * @param {!goog.i18n.NumberFormatSymbolsType.Type=} opt_symbols Optional number
 *     format symbols map, analogous to goog.i18n.NumberFormatSymbols. If
 *     present, this overrides the symbols from the current locale, such as the
 *     percent sign and minus sign.
 * @constructor
 */
goog.i18n.NumberFormat = function(
    pattern, opt_currency, opt_currencyStyle, opt_symbols) {
  'use strict';
  if (opt_currency && !goog.i18n.currency.isValid(opt_currency)) {
    throw new TypeError('Currency must be valid ISO code');
  }

  /**
   * Remember if the implementation is ECMAScript
   * @private {?goog.global.Intl.NumberFormat}
   */
  this.intlFormatter_ = null;
  /** @private {boolean} */
  this.resetSignificantDigits_ = false;
  /** @private {boolean} */
  this.resetFractionDigits_ = false;

  /** @const @private {?string} */
  this.intlCurrencyCode_ = opt_currency ? opt_currency.toUpperCase() : null;

  /** @const @private {number} */
  this.currencyStyle_ =
      opt_currencyStyle || goog.i18n.NumberFormat.CurrencyStyle.LOCAL;

  /** @const @private {?Object<string, string>} */
  this.overrideNumberFormatSymbols_ = opt_symbols || null;

  /** @private {number} */
  this.maximumIntegerDigits_ = 40;
  /** @private {number} */
  this.minimumIntegerDigits_ = 1;
  /** @private {number} */
  this.significantDigits_ = 0;  // invariant, <= maximumFractionDigits
  /** @private {number} */
  this.maximumFractionDigits_ = 3;  // invariant, >= minFractionDigits
  /** @private {number} */
  this.minimumFractionDigits_ = 0;
  /** @private {number} */
  this.minExponentDigits_ = 0;
  /** @private {boolean} */
  this.useSignForPositiveExponent_ = false;

  /**
   * Whether to show trailing zeros in the fraction when significantDigits_ is
   * positive.
   * @private {boolean}
   */
  this.showTrailingZeros_ = false;

  /** @private {string} */
  this.positivePrefix_ = '';
  /** @private {string} */
  this.positiveSuffix_ = '';
  /** @private {string} */
  this.negativePrefix_ = this.getNumberFormatSymbols_().MINUS_SIGN;
  /** @private {string} */
  this.negativeSuffix_ = '';

  // The multiplier for use in percent, per mille, etc.
  /** @private {number} */
  this.multiplier_ = 1;

  /**
   * True if the percent/permill sign of the negative pattern is expected.
   * @private {boolean}
   */
  this.negativePercentSignExpected_ = false;

  /**
   * The grouping array is used to store the values of each number group
   * following left of the decimal place. For example, a number group with
   * goog.i18n.NumberFormat('#,##,###') should have [3,2] where 2 is the
   * repeated number group following a fixed number grouping of size 3.
   * @private {!Array<number>}
   */
  this.groupingArray_ = [];

  /** @private {boolean} */
  this.decimalSeparatorAlwaysShown_ = false;
  /** @private {boolean} */
  this.useExponentialNotation_ = false;
  /** @private {!goog.i18n.NumberFormat.CompactStyle} */
  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;

  // Original numeric pattern. Needed because JS modifies this.pattern_*/
  /** @private {number} */
  this.inputPattern_ = (typeof pattern === 'number') ? pattern : -1;

  /** @private {string} */
  this.pattern_ = (typeof pattern === 'string') ? pattern : '';

  if (goog.i18n.NumberFormat.USE_ECMASCRIPT_I18N_NUMFORMAT &&
      (typeof pattern === 'number')) {
    // Use Native mode for regular patterns
    this.SetUpIntlFormatter_(this.inputPattern_);
  } else {
    // JavaScript implementation for older browswer or a string pattern.
    this.setFormatterToPolyfill_(pattern);
  }
};

// For referencing goog.i18n.USE_ECMASCRIPT_I18N_NUMFORMAT to determine
// compile-time choice of ECMAScript vs. JavaScript implementation
// and data.

/** {boolean} */
goog.i18n.NumberFormat.USE_ECMASCRIPT_I18N_NUMFORMAT =
    goog.FEATURESET_YEAR >= 2020;

/**
 * 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;


/**
 * Native digit codes in EMACScript Intl objects for locales
 * where native digits are prescribed and Intl data is
 * generally available.
 * @private
 */
goog.i18n.NumberFormat.NativeLocaleDigits_ = {
  'ar': 'latn',
  'ar-EG': 'arab',
  'bn': 'beng',
  'fa': 'arabext',
  'mr': 'deva',
  'my': 'mymr',
  'ne': 'deva'
};
/**
 * Set if the usage of Ascii digits in formatting should be enforced.
 * NOTE: This function must be called before constructing NumberFormat.
 *
 * @param {boolean} doEnforce Boolean value about if Ascii digits should be
 *     enforced.
 */
goog.i18n.NumberFormat.setEnforceAsciiDigits = function(doEnforce) {
  'use strict';
  goog.i18n.NumberFormat.enforceAsciiDigits_ = doEnforce;
};


/**
 * Return if Ascii digits is enforced.
 * @return {boolean} If Ascii digits is enforced.
 */
goog.i18n.NumberFormat.isEnforceAsciiDigits = function() {
  'use strict';
  return goog.i18n.NumberFormat.enforceAsciiDigits_;
};


/**
 * Returns the current NumberFormatSymbols.
 * @return {?}
 * @private
 */
goog.i18n.NumberFormat.prototype.getNumberFormatSymbols_ = function() {
  'use strict';
  return this.overrideNumberFormatSymbols_ ||
      (goog.i18n.NumberFormat.enforceAsciiDigits_ ?
           goog.i18n.NumberFormatSymbols_u_nu_latn :
           goog.i18n.NumberFormatSymbols);
};


/**
 * Returns the currency code.
 * @return {string}
 * @private
 */
goog.i18n.NumberFormat.prototype.getCurrencyCode_ = function() {
  'use strict';
  return this.intlCurrencyCode_ ||
      this.getNumberFormatSymbols_().DEF_CURRENCY_CODE;
};


/**
 * 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) {
  'use strict';
  if (this.significantDigits_ > 0 && min > 0) {
    throw new Error(
        'Can\'t combine significant digits and minimum fraction digits');
  }
  // Even if it's the same value, remember the intention to reset the value.
  this.resetFractionDigits_ = true;
  this.minimumFractionDigits_ = min;
  return this;
};


/**
 * Gets minimum number of fraction digits.
 * @return {number} The number of minimum fraction digits.
 */
goog.i18n.NumberFormat.prototype.getMinimumFractionDigits = function() {
  'use strict';
  return this.minimumFractionDigits_;
};


/**
 * 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) {
  'use strict';
  if (max > 308) {
    // Math.pow(10, 309) becomes Infinity which breaks the logic in this class.
    throw new Error('Unsupported maximum fraction digits: ' + max);
  }
  // Even if it's the same value, remember the intention to reset the value.
  this.resetFractionDigits_ = true;
  this.maximumFractionDigits_ = max;
  return this;
};


/**
 * Gets maximum number of fraction digits.
 * @return {number} The number of maximum fraction digits.
 */
goog.i18n.NumberFormat.prototype.getMaximumFractionDigits = function() {
  'use strict';
  return this.maximumFractionDigits_;
};

/**
 * Sets number of significant digits to show. Only fractions will be rounded.
 * Regardless of the number of significant digits set, the number of fractional
 * digits shown will always be capped by the maximum number of fractional digits
 * set on {@link #setMaximumFractionDigits}.
 * @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) {
  'use strict';
  if (this.minimumFractionDigits_ > 0 && number >= 0) {
    throw new Error(
        'Can\'t combine significant digits and minimum fraction digits');
  }
  if (number !== this.significantDigits_) {
    this.resetSignificantDigits_ = true;
  }
  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() {
  'use strict';
  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) {
  'use strict';
  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) {
  'use strict';
  goog.asserts.assert(
      baseFormattingNumber === null || 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() {
  'use strict';
  return this.baseFormattingNumber_;
};

/**
 * Set formatter to polyfill mode when Intl doesn't support Closure features.
 * Called when setting some formatter behavior after constructing the Intl
 * object.
 * @param {number|string} pattern Value to initialize object in JavaScript.
 * @private
 */
goog.i18n.NumberFormat.prototype.setFormatterToPolyfill_ = function(pattern) {
  // JavaScript implementation for older browswer or a string pattern.
  this.intlFormatter_ = null;
  if (typeof pattern === 'number') {
    this.applyStandardPattern_(pattern);
  } else {
    this.applyPattern_(pattern);
  }
};

/**
 * Apply provided pattern, result are stored in member variables.
 *
 * @param {string} pattern String pattern being applied.
 * @private
 */
goog.i18n.NumberFormat.prototype.applyPattern_ = function(pattern) {
  'use strict';
  this.pattern_ = pattern.replace(/ /g, '\u00a0');
  const pos = [0];

  this.positivePrefix_ = this.parseAffix_(pattern, pos);
  const trunkStart = pos[0];
  this.parseTrunk_(pattern, pos);
  const 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]++;
    if (this.multiplier_ != 1) this.negativePercentSignExpected_ = true;
    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.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) {
  'use strict';
  switch (patternType) {
    case goog.i18n.NumberFormat.Format.DECIMAL:
      this.applyPattern_(this.getNumberFormatSymbols_().DECIMAL_PATTERN);
      break;
    case goog.i18n.NumberFormat.Format.SCIENTIFIC:
      this.applyPattern_(this.getNumberFormatSymbols_().SCIENTIFIC_PATTERN);
      break;
    case goog.i18n.NumberFormat.Format.PERCENT:
      this.applyPattern_(this.getNumberFormatSymbols_().PERCENT_PATTERN);
      break;
    case goog.i18n.NumberFormat.Format.CURRENCY:
      this.applyPattern_(goog.i18n.currency.adjustPrecision(
          this.getNumberFormatSymbols_().CURRENCY_PATTERN,
          this.getCurrencyCode_()));
      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 new 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) {
  'use strict';
  this.compactStyle_ = style;
  this.applyPattern_(this.getNumberFormatSymbols_().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) {
  'use strict';
  let pos = opt_pos || [0];

  if (this.compactStyle_ !== goog.i18n.NumberFormat.CompactStyle.NONE) {
    throw new Error('Parsing of compact numbers is unimplemented');
  }

  let ret = NaN;

  // We don't want to handle multiple kinds of space in parsing, normalize the
  // regular and narrow nbsp to nbsp.
  text = text.replace(/ |\u202f/g, '\u00a0');

  let gotPositive = text.indexOf(this.positivePrefix_, pos[0]) == pos[0];
  let 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(this.getNumberFormatSymbols_().INFINITY, pos[0]) == pos[0]) {
    pos[0] += this.getNumberFormatSymbols_().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) {
  'use strict';
  let sawDecimal = false;
  let sawExponent = false;
  let sawDigit = false;
  let exponentPos = -1;
  let scale = 1;
  const decimal = this.getNumberFormatSymbols_().DECIMAL_SEP;
  let grouping = this.getNumberFormatSymbols_().GROUP_SEP;
  const exponentChar = this.getNumberFormatSymbols_().EXP_SYMBOL;

  if (this.compactStyle_ != goog.i18n.NumberFormat.CompactStyle.NONE) {
    throw new Error('Parsing of compact style numbers is not implemented');
  }

  // We don't want to handle multiple kinds of space in parsing, normalize the
  // narrow nbsp to nbsp.
  grouping = grouping.replace(/\u202f/g, '\u00a0');

  let normalizedText = '';
  for (; pos[0] < text.length; pos[0]++) {
    const ch = text.charAt(pos[0]);
    const 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;
      exponentPos = pos[0];
    } else if (ch == '+' || ch == '-') {
      // Stop parsing if a '+' or '-' sign is found after digits have been found
      // but it's not located right after an exponent sign.
      if (sawDigit && exponentPos != pos[0] - 1) {
        break;
      }
      normalizedText += ch;
    } else if (
        this.multiplier_ == 1 &&
        ch == this.getNumberFormatSymbols_().PERCENT.charAt(0)) {
      // Parse the percent character as part of the number only when it's
      // not already included in the pattern.
      if (scale != 1) {
        break;
      }
      scale = 100;
      if (sawDigit) {
        pos[0]++;  // eat this character if parse end here
        break;
      }
    } else if (
        this.multiplier_ == 1 &&
        ch == this.getNumberFormatSymbols_().PERMILL.charAt(0)) {
      // Parse the permill character as part of the number only when it's
      // not already included in the pattern.
      if (scale != 1) {
        break;
      }
      scale = 1000;
      if (sawDigit) {
        pos[0]++;  // eat this character if parse end here
        break;
      }
    } else {
      break;
    }
  }

  // Scale the number when the percent/permill character was included in
  // the pattern.
  if (this.multiplier_ != 1) {
    scale = this.multiplier_;
  }

  return parseFloat(normalizedText) / scale;
};

/**
 * Creates Intl native formatter based on settings.
 * Uses pattern type if integer.
 * If a string, try to parse or derive parameters for native mode.
 * @param {number} inputPattern numeric indicator of standard pattern
 * @private
 */
goog.i18n.NumberFormat.prototype.SetUpIntlFormatter_ = function(inputPattern) {
  /** @type {!goog.i18n.NumberFormat.IntlOptions} */
  const options = {
    notation: 'standard',  // Default
    minimumIntegerDigits: Math.min(21, Math.max(1, this.minimumIntegerDigits_))
  };

  // Create notation based on a enum value.
  if (this.useSignForPositiveExponent_) {
    options.signDisplay = 'always';
  }

  if (goog.i18n.NumberFormat.isEnforceAsciiDigits()) {
    options.numberingSystem = 'latn';
  }

  // If changed from defaults, specify significant or fraction digits.
  if (this.resetSignificantDigits_) {
    options.minimumSignificantDigits = 1;
    options.maximumSignificantDigits =
        Math.max(1, Math.min(21, this.significantDigits_));
  } else if (this.resetFractionDigits_) {
    options.minimumFractionDigits = Math.max(0, this.minimumFractionDigits_);
    options.maximumFractionDigits =
        Math.min(20, Math.max(0, this.maximumFractionDigits_));
  }

  switch (inputPattern) {
    case goog.i18n.NumberFormat.Format.DECIMAL:
      options.style = 'decimal';
      break;
    case goog.i18n.NumberFormat.Format.SCIENTIFIC:
      options.notation = 'scientific';
      // Special case for scientific notation
      options.maximumFractionDigits =
          Math.min(20, Math.max(0, this.minExponentDigits_));
      break;
    case goog.i18n.NumberFormat.Format.PERCENT:
      options.style = 'percent';
      break;
    case goog.i18n.NumberFormat.Format.CURRENCY:
      // From NumberFormatSymbols.
      options.style = 'currency';
      const currencyCode = this.getCurrencyCode_();
      options.currency = currencyCode;

      // Get the precision based on currency code, if available.
      // Mask off bits regarding formatting.
      const precision = goog.i18n.currency.isAvailable(currencyCode) ?
          goog.i18n.currency.CurrencyInfo[currencyCode][0] % 16 :
          2;

      if (this.resetFractionDigits_) {
        // Fraction digits were purposely reset, so use those rather than
        // default for currency.
        options.minimumFractionDigits =
            Math.max(this.minimumFractionDigits_, 0);
        options.maximumFractionDigits =
            Math.min(this.maximumFractionDigits_, 20);
      } else {
        // Use default currency fraction digits.
        options.minimumFractionDigits = Math.max(0, precision);
        options.maximumFractionDigits =
            Math.min(options.minimumFractionDigits, 20);
      }

      // Map Closure currency styles to Intl currencyDisplay.
      switch (this.currencyStyle_) {
        default:
        case goog.i18n.NumberFormat.CurrencyStyle.PORTABLE:
          options.currencyDisplay = 'symbol';
          break;
        case goog.i18n.NumberFormat.CurrencyStyle.GLOBAL:
          options.currencyDisplay = 'code';
          break;
        case goog.i18n.NumberFormat.CurrencyStyle.LOCAL:
          options.currencyDisplay = 'narrowSymbol';
      }
      break;
    case goog.i18n.NumberFormat.Format.COMPACT_SHORT:
      this.compactStyle_ = goog.i18n.NumberFormat.CompactStyle.SHORT;
      options.notation = 'compact';
      options.compactDisplay = 'short';
      break;
    case goog.i18n.NumberFormat.Format.COMPACT_LONG:
      this.compactStyle_ = goog.i18n.NumberFormat.CompactStyle.LONG;
      options.notation = 'compact';
      options.compactDisplay = 'long';
      break;
    default:
      throw new Error(
          'Unsupported ECMAScript NumberFormat custom pattern = ' +
          this.pattern_);
  }

  try {
    // Create Intl formatter.
    let locale;
    if (goog.LOCALE) {
      locale = goog.LOCALE.replace('_', '-');
    }
    if (!goog.i18n.NumberFormat.enforceAsciiDigits_ &&
        locale in goog.i18n.NumberFormat.NativeLocaleDigits_) {
      // Sets native digits for same locales as polyfill.
      options.numberingSystem =
          goog.i18n.NumberFormat.NativeLocaleDigits_[locale];
    }
    // This works with undefined locale or empty string.
    this.intlFormatter_ = new Intl.NumberFormat(locale, options);
  } catch (error) {
    this.intlFormatter_ = null;
    throw new Error('ECMAScript NumberFormat error: ' + error);
  }
  this.resetSignificantDigits_ = false;
  this.resetFractionDigits_ = false;
};

/**
 * Checks options to see if the native formatter needs to be
 * remade.
 * @return {boolean} True if options have changed.
 */
goog.i18n.NumberFormat.prototype.NativeOptionsChanged_ = function() {
  return (
      this.resetSignificantDigits_ || this.resetFractionDigits_ ||
      goog.i18n.NumberFormat.enforceAsciiDigits_);
};

/**
 * 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) {
  'use strict';
  // Check for compatibility with polyfill implementation.
  if (this.minimumFractionDigits_ > this.maximumFractionDigits_) {
    throw new Error('Min value must be less than max value');
  }
  if (goog.i18n.NumberFormat.USE_ECMASCRIPT_I18N_NUMFORMAT &&
      this.intlFormatter_) {
    return this.formatUsingNativeMode_(number);
  }
  // Format using JavaScript
  if (isNaN(number)) {
    return this.getNumberFormatSymbols_().NAN;
  }

  const parts = [];
  const baseFormattingNumber = (this.baseFormattingNumber_ === null) ?
      number :
      this.baseFormattingNumber_;
  const unit = this.getUnitAfterRounding_(baseFormattingNumber, number);
  number = goog.i18n.NumberFormat.decimalShift_(number, -unit.divisorBase);

  // in icu code, it is commented that certain computation need to keep the
  // negative sign for 0.
  const isNegative = number < 0.0 || number == 0.0 && 1 / number < 0.0;

  if (isNegative) {
    // Also handle compact number formats
    if (unit.negative_prefix) {
      // Compact form includes the negative sign
      parts.push(unit.negative_prefix);
    } else {
      parts.push(unit.prefix);
      parts.push(this.negativePrefix_);
    }
  } else {
    parts.push(unit.prefix);
    parts.push(this.positivePrefix_);
  }

  if (!isFinite(number)) {
    parts.push(this.getNumberFormatSymbols_().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);
  }

  if (isNegative) {
    // Also handle compact number formats
    if (unit.negative_suffix) {
      // Compact form includes the negative sign
      parts.push(unit.negative_suffix);
    } else {
      if (isFinite(number)) {
        // Add scientific or compact suffix only for finite values.
        // Infinity does not have a scientific or compact suffix in ECMA-402.
        parts.push(unit.suffix);
      }
      parts.push(this.negativeSuffix_);
    }
  } else {
    if (isFinite(number)) {
      // Add scientific or compact suffix only for finite values.
      parts.push(unit.suffix);
    }
    parts.push(this.positiveSuffix_);
  }
  return parts.join('');
};

/**
 * Formats a Number to produce a string using Intl NumberFormat class.
 *
 * @param {number} number The Number to be formatted.
 * @return {string} The formatted number string.
 * @private
 */
goog.i18n.NumberFormat.prototype.formatUsingNativeMode_ = function(number) {
  if (this.intlFormatter_.format == null || this.NativeOptionsChanged_()) {
    // Need to recreate the native formatter.
    this.SetUpIntlFormatter_(this.inputPattern_);
  }

  // Special case for significant digits and fractional number because
  // the JavaScript version applies significant digits to the
  // fraction part, not just the whole number portion.
  if (Math.abs(number) < 1 &&
      this.significantDigits_ > this.maximumFractionDigits_) {
    // Round using the maximum number of fraction digits
    const multipler = Math.pow(10, this.maximumFractionDigits_);
    const newNum = Math.abs(number) * multipler;
    const rounded = Math.round(newNum) * Math.sign(number);
    number = rounded / multipler;
  }

  // Create custom output when specialized percentage is requested.
  /** @type {!goog.i18n.NumberFormat.IntlOptions} */
  const options = this.intlFormatter_.resolvedOptions();
  if (options.style === 'percent' && this.overrideNumberFormatSymbols_ &&
      this.overrideNumberFormatSymbols_['PERCENT']) {
    /** @type {!Array<!goog.i18n.NumberFormat.FormattedPart>} */
    const resultParts = this.intlFormatter_.formatToParts(number);
    const percentReplacement = this.overrideNumberFormatSymbols_['PERCENT'];
    // Return with custom percent symbol 'percentSign'
    const parts = resultParts.map(
        (element) => element.type === 'percentSign' ? percentReplacement :
                                                      element.value);
    return parts.join('');
  }

  if (this.showTrailingZeros_) {
    // Trailing zeros requires more complex handling.
    /** @type {!Array<!goog.i18n.NumberFormat.FormattedPart>} */
    const resultParts = this.intlFormatter_.formatToParts(number);

    // Count digits in integer part except leading zeros.
    let intSize = 0;
    resultParts.forEach(
        element => element.type === 'integer' && element.value !== '0' ?
            intSize += element.value.length :
            0);

    let fracSize = 0;
    for (let i = 0; i < resultParts.length; i++) {
      if (resultParts[i].type === 'fraction') {
        fracSize += resultParts[i].value.length;
      }
    }

    if (intSize + fracSize < this.significantDigits_) {
      // Create temporary formatter for rendering this condition
      delete options['minimumSignificantDigits'];
      delete options['maximumSignificantDigits'];
      options.minimumFractionDigits = this.significantDigits_ - intSize;
      this.resetFractionDigits_ = true;
      try {
        const newIntlFormatter = new Intl.NumberFormat(options.locale, options);
        return newIntlFormatter.format(number);
      } catch {
        // Fall back if Intl constructor fails.
        return this.intlFormatter_.format(number);
      }
    }
  }

  if (this.baseFormattingNumber_) {
    // Get the compact form for the base number.
    /** @type {!Array<!goog.i18n.NumberFormat.FormattedPart>} */
    const scaledResult =
        this.intlFormatter_.formatToParts(this.baseFormattingNumber_);
    // Adjust format options for a standard number with correct digit count.
    delete options['compactDisplay'];
    options.notation = 'standard';
    if (number != 0) {
      const scaledForFraction = this.baseFormattingNumber_ / number;
      let fractionDigits = 0;
      if (Math.abs(scaledForFraction) > 1.0) {
        fractionDigits = this.intLog10_(this.baseFormattingNumber_ / number);
        if (Math.round(scaledForFraction) != scaledForFraction) {
          fractionDigits +=
              1;  // Avoid an extra decimal place if exact divisor.
        }
      }
      if (!Number.isNaN(fractionDigits) && !options.minimumFractionDigits) {
        options.minimumFractionDigits = fractionDigits;
        options.maximumFractionDigits = !options.maximumFractionDigits ?
            fractionDigits :
            Math.max(options.maximumFractionDigits, fractionDigits);
      } else {
        // Special case to prevent extra digits in result.
        options.maximumFractionDigits = options.minimumFractionDigits;
      }
      delete options['minimumSignificantDigits'];
      delete options['maximumSignificantDigits'];
    }

    const baseFormattingNumber = (this.baseFormattingNumber_ === null) ?
        number :
        this.baseFormattingNumber_;
    const unit = this.getUnitAfterRounding_(baseFormattingNumber, number);
    const reducedNumber =
        goog.i18n.NumberFormat.decimalShift_(number, -unit.divisorBase);

    let reducedFormatter;
    try {
      reducedFormatter = new Intl.NumberFormat(options.locale, options);
    } catch {
      // Fall back if Intl constructor fails.
      return this.intlFormatter_.format(number);
    }

    // Combine reduced result number parts with the scaled compact form.
    /** @type {!Array<!goog.i18n.NumberFormat.FormattedPart>} */
    const reducedResult = reducedFormatter.formatToParts(reducedNumber);
    const baseFormattedParts = reducedResult.map(
        (element) =>
            (element.type === 'integer' || element.type === 'group' ||
             element.type === 'decimal' || element.type === 'fraction') ?
            element.value :
            '');
    // Add compact and literal parts from scaled result.
    const compactAdditions =
        scaledResult
            .filter(
                entry => entry.type === 'compact' || entry.type === 'literal')
            .map(entry => entry.value);
    return baseFormattedParts.concat(compactAdditions).join('');
  }

  // No special formatting.
  return this.intlFormatter_.format(number);
};

/**
 * 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) {
  'use strict';
  const shift = goog.i18n.NumberFormat.decimalShift_;

  let shiftedNumber = shift(number, this.maximumFractionDigits_);
  if (this.significantDigits_ > 0) {
    shiftedNumber = this.roundToSignificantDigits_(
        shiftedNumber, this.significantDigits_, this.maximumFractionDigits_);
  }
  shiftedNumber = Math.round(shiftedNumber);

  let intValue, fracValue;
  if (isFinite(shiftedNumber)) {
    intValue = Math.floor(shift(shiftedNumber, -this.maximumFractionDigits_));
    fracValue = Math.floor(
        shiftedNumber - shift(intValue, this.maximumFractionDigits_));
  } else {
    intValue = number;
    fracValue = 0;
  }
  return {intValue: intValue, fracValue: fracValue};
};


/**
 * Formats a number with the appropriate groupings when there are repeating
 * digits present. Repeating digits exists when the length of the digits left
 * of the decimal place exceeds the number of non-repeating digits.
 *
 * Formats a number by iterating through the integer number (intPart) from the
 * most left of the decimal place by inserting the appropriate number grouping
 * separator for the repeating digits until all of the repeating digits is
 * iterated. Then iterate through the non-repeating digits by inserting the
 * appropriate number grouping separator until all the non-repeating digits
 * is iterated through.
 *
 * In the number grouping concept, anything left of the decimal
 * place is followed by non-repeating digits and then repeating digits. If the
 * pattern is #,##,###, then we first (from the left of the decimal place) have
 * a non-repeating digit of size 3 followed by repeating digits of size 2
 * separated by a thousand separator. If the length of the digits are six or
 * more, there may be repeating digits required. For example, the value of
 * 12345678 would format as 1,23,45,678 where the repeating digit is length 2.
 *
 * @param {!Array<string>} parts An array to build the 'parts' of the formatted
 *  number including the values and separators.
 * @param {number} zeroCode The value of the zero digit whether or not
 *  goog.i18n.NumberFormat.enforceAsciiDigits_ is enforced.
 * @param {string} intPart The integer representation of the number to be
 *  formatted and referenced.
 * @param {!Array<number>} groupingArray The array of numbers to determine the
 *  grouping of repeated and non-repeated digits.
 * @param {number} repeatedDigitLen The length of the repeated digits left of
 *  the non-repeating digits left of the decimal.
 * @return {!Array<string>} Returns the resulting parts variable containing
 *  how numbers are to be grouped and appear.
 * @private
 */
goog.i18n.NumberFormat.prototype.formatNumberGroupingRepeatingDigitsParts_ =
    function(parts, zeroCode, intPart, groupingArray, repeatedDigitLen) {
  'use strict';
  // Keep track of how much has been completed on the non repeated groups
  let nonRepeatedGroupCompleteCount = 0;
  let currentGroupSizeIndex = 0;
  let currentGroupSize = 0;

  const grouping = this.getNumberFormatSymbols_().GROUP_SEP;
  const digitLen = intPart.length;

  // There are repeating digits and non-repeating digits
  for (let i = 0; i < digitLen; i++) {
    parts.push(String.fromCharCode(zeroCode + Number(intPart.charAt(i)) * 1));
    if (digitLen - i > 1) {
      currentGroupSize = groupingArray[currentGroupSizeIndex];
      if (i < repeatedDigitLen) {
        // Process the left side (the repeated number groups)
        let repeatedDigitIndex = repeatedDigitLen - i;
        // Edge case if there's a number grouping asking for "1" group at
        // a time; otherwise, if the remainder is 1, there's the separator
        if (currentGroupSize === 1 ||
            (currentGroupSize > 0 &&
             (repeatedDigitIndex % currentGroupSize) === 1)) {
          parts.push(grouping);
        }
      } else if (currentGroupSizeIndex < groupingArray.length) {
        // Process the right side (the non-repeated fixed number groups)
        if (i === repeatedDigitLen) {
          // Increase the group index because a separator
          // has previously added in the earlier logic
          currentGroupSizeIndex += 1;
        } else if (
            currentGroupSize ===
            i - repeatedDigitLen - nonRepeatedGroupCompleteCount + 1) {
          // Otherwise, just iterate to the right side and
          // add a separator once the length matches to the expected
          parts.push(grouping);
          // Keep track of what has been completed on the right
          nonRepeatedGroupCompleteCount += currentGroupSize;
          currentGroupSizeIndex += 1;  // Get to the next number grouping
        }
      }
    }
  }
  return parts;
};


/**
 * Formats a number with the appropriate groupings when there are no repeating
 * digits present. Non-repeating digits exists when the length of the digits
 * left of the decimal place is equal or lesser than the length of
 * non-repeating digits.
 *
 * Formats a number by iterating through the integer number (intPart) from the
 * right most non-repeating number group of the decimal place. For each group,
 * inserting the appropriate number grouping separator for the non-repeating
 * digits until the number is completely iterated.
 *
 * In the number grouping concept, anything left of the decimal
 * place is followed by non-repeating digits and then repeating digits. If the
 * pattern is #,##,###, then we first (from the left of the decimal place) have
 * a non-repeating digit of size 3 followed by repeating digits of size 2
 * separated by a thousand separator. If the length of the digits are five or
 * less, there won't be any repeating digits required. For example, the value
 * of 12345 would be formatted as 12,345 where the non-repeating digit is of
 * length 3.
 *
 * @param {!Array<string>} parts An array to build the 'parts' of the formatted
 *  number including the values and separators.
 * @param {number} zeroCode The value of the zero digit whether or not
 *  goog.i18n.NumberFormat.enforceAsciiDigits_ is enforced.
 * @param {string} intPart The integer representation of the number to be
 *  formatted and referenced.
 * @param {!Array<number>} groupingArray The array of numbers to determine the
 *  grouping of repeated and non-repeated digits.
 * @return {!Array<string>} Returns the resulting parts variable containing
 *  how numbers are to be grouped and appear.
 * @private
 */
goog.i18n.NumberFormat.prototype.formatNumberGroupingNonRepeatingDigitsParts_ =
    function(parts, zeroCode, intPart, groupingArray) {
  'use strict';
  // Keep track of how much has been completed on the non repeated groups
  const grouping = this.getNumberFormatSymbols_().GROUP_SEP;
  let currentGroupSizeIndex;
  let currentGroupSize = 0;
  let digitLenLeft = intPart.length;
  const rightToLeftParts = [];

  // Start from the right most non-repeating group and work inwards
  for (currentGroupSizeIndex = groupingArray.length - 1;
       currentGroupSizeIndex >= 0 && digitLenLeft > 0;
       currentGroupSizeIndex--) {
    currentGroupSize = groupingArray[currentGroupSizeIndex];
    // Iterate from the right most digit
    for (let rightDigitIndex = 0; rightDigitIndex < currentGroupSize &&
         ((digitLenLeft - rightDigitIndex - 1) >= 0);
         rightDigitIndex++) {
      rightToLeftParts.push(String.fromCharCode(
          zeroCode +
          Number(intPart.charAt(digitLenLeft - rightDigitIndex - 1)) * 1));
    }
    // Update the number of digits left
    digitLenLeft -= currentGroupSize;
    if (digitLenLeft > 0) {
      rightToLeftParts.push(grouping);
    }
  }
  // Reverse and push onto the remaining parts
  parts.push.apply(parts, rightToLeftParts.reverse());

  return parts;
};


/**
 * 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) {
  'use strict';
  if (this.minimumFractionDigits_ > this.maximumFractionDigits_) {
    throw new Error('Min value must be less than max value');
  }

  if (!parts) {
    parts = [];
  }

  const rounded = this.roundNumber_(number);
  const intValue = rounded.intValue;
  const fracValue = rounded.fracValue;

  const numIntDigits = (intValue == 0) ? 0 : this.intLog10_(intValue) + 1;
  const fractionPresent = this.minimumFractionDigits_ > 0 || fracValue > 0 ||
      (this.showTrailingZeros_ && numIntDigits < this.significantDigits_);
  let minimumFractionDigits = this.minimumFractionDigits_;
  if (fractionPresent) {
    if (this.showTrailingZeros_ && this.significantDigits_ > 0) {
      minimumFractionDigits = this.significantDigits_ - numIntDigits;
    } else {
      minimumFractionDigits = this.minimumFractionDigits_;
    }
  }

  let intPart = '';
  let translatableInt = intValue;
  while (translatableInt > 1E20) {
    // here it goes beyond double precision, add '0' make it look better
    intPart = '0' + intPart;
    translatableInt =
        Math.round(goog.i18n.NumberFormat.decimalShift_(translatableInt, -1));
  }
  intPart = translatableInt + intPart;

  const decimal = this.getNumberFormatSymbols_().DECIMAL_SEP;
  const zeroCode = this.getNumberFormatSymbols_().ZERO_DIGIT.charCodeAt(0);
  const digitLen = intPart.length;
  let nonRepeatedGroupCount = 0;

  if (intValue > 0 || minIntDigits > 0) {
    for (let i = digitLen; i < minIntDigits; i++) {
      parts.push(String.fromCharCode(zeroCode));
    }

    // If there's more than 1 number grouping,
    // figure out the length of the non-repeated groupings (on the right)
    if (this.groupingArray_.length >= 2) {
      for (let j = 1; j < this.groupingArray_.length; j++) {
        nonRepeatedGroupCount += this.groupingArray_[j];
      }
    }

    // Anything left of the fixed number grouping is repeated,
    // figure out the length of repeated groupings (on the left)
    const repeatedDigitLen = digitLen - nonRepeatedGroupCount;
    if (repeatedDigitLen > 0) {
      // There are repeating digits and non-repeating digits
      parts = this.formatNumberGroupingRepeatingDigitsParts_(
          parts, zeroCode, intPart, this.groupingArray_, repeatedDigitLen);
    } else {
      // There are no repeating digits and only non-repeating digits
      parts = this.formatNumberGroupingNonRepeatingDigitsParts_(
          parts, zeroCode, intPart, this.groupingArray_);
    }
  } 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);
  }

  let fracPart = String(fracValue);
  // Handle case where fracPart is in scientific notation.
  const fracPartSplit = fracPart.split('e+');
  if (fracPartSplit.length == 2) {
    // Only keep significant digits.
    const floatFrac = parseFloat(fracPartSplit[0]);
    fracPart = String(
        this.roundToSignificantDigits_(floatFrac, this.significantDigits_, 1));
    fracPart = fracPart.replace('.', '');
    // Append zeroes based on the exponent.
    const exp = parseInt(fracPartSplit[1], 10);
    fracPart += goog.string.repeat('0', exp - fracPart.length + 1);
  }

  // Add Math.pow(10, this.maximumFractionDigits) to fracPart. Uses string ops
  // to avoid complexity with scientific notation and overflows.
  if (this.maximumFractionDigits_ + 1 > fracPart.length) {
    const zeroesToAdd = this.maximumFractionDigits_ - fracPart.length;
    fracPart = '1' + goog.string.repeat('0', zeroesToAdd) + fracPart;
  }

  let fracLen = fracPart.length;
  while (fracPart.charAt(fracLen - 1) == '0' &&
         fracLen > minimumFractionDigits + 1) {
    fracLen--;
  }

  for (let i = 1; i < fracLen; i++) {
    parts.push(String.fromCharCode(zeroCode + Number(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) {
  'use strict';
  parts.push(this.getNumberFormatSymbols_().EXP_SYMBOL);

  if (exponent < 0) {
    exponent = -exponent;
    parts.push(this.getNumberFormatSymbols_().MINUS_SIGN);
  } else if (this.useSignForPositiveExponent_) {
    parts.push(this.getNumberFormatSymbols_().PLUS_SIGN);
  }

  const exponentDigits = '' + exponent;
  const zeroChar = this.getNumberFormatSymbols_().ZERO_DIGIT;
  for (let i = exponentDigits.length; i < this.minExponentDigits_; i++) {
    parts.push(zeroChar);
  }
  parts.push(exponentDigits);
};

/**
 * Returns the mantissa for the given value and its exponent.
 *
 * @param {number} value
 * @param {number} exponent
 * @return {number}
 * @private
 */
goog.i18n.NumberFormat.prototype.getMantissa_ = function(value, exponent) {
  'use strict';
  return goog.i18n.NumberFormat.decimalShift_(value, -exponent);
};

/**
 * Formats Number in exponential format.
 *
 * @param {number} number Value need to be formatted.
 * @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) {
  'use strict';
  if (number == 0.0) {
    this.subformatFixed_(number, this.minimumIntegerDigits_, parts);
    this.addExponentPart_(0, parts);
    return;
  }

  let exponent = goog.math.safeFloor(Math.log(number) / Math.log(10));
  number = this.getMantissa_(number, exponent);

  let 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.
    let remainder = exponent % this.maximumIntegerDigits_;
    if (remainder < 0) {
      remainder = this.maximumIntegerDigits_ + remainder;
    }

    number = goog.i18n.NumberFormat.decimalShift_(number, remainder);
    exponent -= remainder;

    minIntDigits = 1;
  } else {
    // No repeating range is defined; use minimum integer digits.
    if (this.minimumIntegerDigits_ < 1) {
      exponent++;
      number = goog.i18n.NumberFormat.decimalShift_(number, -1);
    } else {
      exponent -= this.minimumIntegerDigits_ - 1;
      number = goog.i18n.NumberFormat.decimalShift_(
          number, 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) {
  'use strict';
  const code = ch.charCodeAt(0);
  // between '0' to '9'
  if (48 <= code && code < 58) {
    return code - 48;
  } else {
    const zeroCode = this.getNumberFormatSymbols_().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 plus character.
 * @type {string}
 * @private
 */
goog.i18n.NumberFormat.PATTERN_PLUS_ = '+';


/**
 * A generic currency sign 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) {
  'use strict';
  let affix = '';
  let inQuote = false;
  const len = pattern.length;

  for (; pos[0] < len; pos[0]++) {
    const 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.getCurrencyCode_();
          } else {
            switch (this.currencyStyle_) {
              case goog.i18n.NumberFormat.CurrencyStyle.LOCAL:
                affix += goog.i18n.currency.getLocalCurrencySignWithFallback(
                    this.getCurrencyCode_());
                break;
              case goog.i18n.NumberFormat.CurrencyStyle.GLOBAL:
                affix += goog.i18n.currency.getGlobalCurrencySignWithFallback(
                    this.getCurrencyCode_());
                break;
              case goog.i18n.NumberFormat.CurrencyStyle.PORTABLE:
                affix += goog.i18n.currency.getPortableCurrencySignWithFallback(
                    this.getCurrencyCode_());
                break;
              default:
                break;
            }
          }
          break;
        case goog.i18n.NumberFormat.PATTERN_PERCENT_:
          if (!this.negativePercentSignExpected_ && this.multiplier_ != 1) {
            throw new Error('Too many percent/permill');
          } else if (
              this.negativePercentSignExpected_ && this.multiplier_ != 100) {
            throw new Error('Inconsistent use of percent/permill characters');
          }
          this.multiplier_ = 100;
          this.negativePercentSignExpected_ = false;
          affix += this.getNumberFormatSymbols_().PERCENT;
          break;
        case goog.i18n.NumberFormat.PATTERN_PER_MILLE_:
          if (!this.negativePercentSignExpected_ && this.multiplier_ != 1) {
            throw new Error('Too many percent/permill');
          } else if (
              this.negativePercentSignExpected_ && this.multiplier_ != 1000) {
            throw new Error('Inconsistent use of percent/permill characters');
          }
          this.multiplier_ = 1000;
          this.negativePercentSignExpected_ = false;
          affix += this.getNumberFormatSymbols_().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) {
  'use strict';
  let decimalPos = -1;
  let digitLeftCount = 0;
  let zeroDigitCount = 0;
  let digitRightCount = 0;
  let groupingCount = -1;
  const len = pattern.length;
  for (let loop = true; pos[0] < len && loop; pos[0]++) {
    const 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 new Error('Unexpected "0" in pattern "' + pattern + '"');
        }
        zeroDigitCount++;
        if (groupingCount >= 0 && decimalPos < 0) {
          groupingCount++;
        }
        break;
      case goog.i18n.NumberFormat.PATTERN_GROUPING_SEPARATOR_:
        if (groupingCount > 0) {
          this.groupingArray_.push(groupingCount);
        }
        groupingCount = 0;
        break;
      case goog.i18n.NumberFormat.PATTERN_DECIMAL_SEPARATOR_:
        if (decimalPos >= 0) {
          throw new Error(
              'Multiple decimal separators in pattern "' + pattern + '"');
        }
        decimalPos = digitLeftCount + zeroDigitCount + digitRightCount;
        break;
      case goog.i18n.NumberFormat.PATTERN_EXPONENT_:
        if (this.useExponentialNotation_) {
          throw new 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 new Error('Malformed exponential pattern "' + pattern + '"');
        }
        loop = false;
        break;
      default:
        pos[0]--;
        loop = false;
        break;
    }
  }

  if (zeroDigitCount == 0 && digitLeftCount > 0 && decimalPos >= 0) {
    // Handle '###.###' and '###.' and '.###'
    let 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 new Error('Malformed pattern "' + pattern + '"');
  }
  const 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.
  const 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;
    }
  }

  // Add another number grouping at the end
  this.groupingArray_.push(Math.max(0, groupingCount));
  this.decimalSeparatorAlwaysShown_ =
      decimalPos == 0 || decimalPos == totalDigits;
};


/**
 * Alias for the compact format 'unit' object.
 * @typedef {{
 *     divisorBase: number,
 *     negative_prefix: string,
 *     negative_suffix: string,
 *     prefix: string,
 *     suffix: string
 * }}
 */
goog.i18n.NumberFormat.CompactNumberUnit;


/**
 * Parameters to Intl.NumberFormat constructor
 * @private @typedef {{
 *    localeMatcher: (string|undefined),
 *    signDisplay: (string|undefined),
 *    notation: (string|undefined),
 *    useGrouping: (boolean|undefined),
 *    numberingSystem: (string|undefined),
 *    style: (string|undefined),
 *    currency: (string|undefined),
 *    currencyDisplay: (string|undefined),
 *    minimumIntegerDigits: (number|undefined),
 *    minimumFractionDigits: (number|undefined),
 *    maximumFractionDigits: (number|undefined),
 *    minimumSignificantDigits: (number|undefined),
 *    maximumSignificantDigits: (number|undefined),
 *    compactDisplay: (string|undefined),
 *    locale: (string|undefined),
 * }}
 */
goog.i18n.NumberFormat.IntlOptions;

/**
 * Return results from formatToParts
 * @private @typedef {{
 *    type: (string),
 *    value: (string)
 * }}
 */
goog.i18n.NumberFormat.FormattedPart;

/**
 * The empty unit, corresponding to a base of 0.
 * @private {!goog.i18n.NumberFormat.CompactNumberUnit}
 */
goog.i18n.NumberFormat.NULL_UNIT_ = {
  divisorBase: 0,
  negative_prefix: '',
  negative_suffix: '',
  prefix: '',
  suffix: ''
};

/**
 * 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) {
  'use strict';
  let table = this.compactStyle_ == goog.i18n.NumberFormat.CompactStyle.SHORT ?
      goog.i18n.CompactNumberFormatSymbols.COMPACT_DECIMAL_SHORT_PATTERN :
      goog.i18n.CompactNumberFormatSymbols.COMPACT_DECIMAL_LONG_PATTERN;

  if (table == null) {
    table = goog.i18n.CompactNumberFormatSymbols.COMPACT_DECIMAL_SHORT_PATTERN;
  }

  if (base < 3) {
    return goog.i18n.NumberFormat.NULL_UNIT_;
  } else {
    const shift = goog.i18n.NumberFormat.decimalShift_;

    base = Math.min(14, base);
    let patterns = table[shift(1, base)];
    let previousNonNullBase = base - 1;
    while (!patterns && previousNonNullBase >= 3) {
      patterns = table[shift(1, previousNonNullBase)];
      previousNonNullBase--;
    }
    if (!patterns) {
      return goog.i18n.NumberFormat.NULL_UNIT_;
    }

    let pattern = patterns[plurality];

    // Return pattern for negative formatting, if present
    let neg_prefix = '';
    let neg_suffix = '';
    let index_of_neg_part = pattern.indexOf(';');
    let neg_pattern = null;
    if (index_of_neg_part >= 0) {
      // Trim positive pattern
      pattern = pattern.substring(0, index_of_neg_part);
      neg_pattern = pattern.substring(index_of_neg_part + 1);
      if (neg_pattern) {
        const neg_parts = /([^0]*)(0+)(.*)/.exec(neg_pattern);
        neg_prefix = neg_parts[1];
        neg_suffix = neg_parts[3];
      }
    }

    if (!pattern || pattern == '0') {
      return goog.i18n.NumberFormat.NULL_UNIT_;
    }

    const parts = /([^0]*)(0+)(.*)/.exec(pattern);
    if (!parts) {
      return goog.i18n.NumberFormat.NULL_UNIT_;
    }

    return {
      divisorBase: (previousNonNullBase + 1) - (parts[2].length - 1),
      negative_prefix: neg_prefix,
      negative_suffix: neg_suffix,
      prefix: parts[1],
      suffix: parts[3]
    };
  }
};


/**
 * 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) {
  'use strict';
  if (this.compactStyle_ == goog.i18n.NumberFormat.CompactStyle.NONE) {
    return goog.i18n.NumberFormat.NULL_UNIT_;
  }

  formattingNumber = Math.abs(formattingNumber);
  pluralityNumber = Math.abs(pluralityNumber);

  const initialPlurality = this.pluralForm_(formattingNumber);
  // Compute the exponent from the formattingNumber, to compute the unit.
  const base = formattingNumber <= 1 ? 0 : this.intLog10_(formattingNumber);
  const initialDivisor = this.getUnitFor_(base, initialPlurality).divisorBase;
  // Round both numbers based on the unit used.
  const pluralityAttempt =
      goog.i18n.NumberFormat.decimalShift_(pluralityNumber, -initialDivisor);
  const pluralityRounded = this.roundNumber_(pluralityAttempt);
  const formattingAttempt =
      goog.i18n.NumberFormat.decimalShift_(formattingNumber, -initialDivisor);
  const formattingRounded = this.roundNumber_(formattingAttempt);
  // Compute the plurality of the pluralityNumber when formatted using the name
  // units as the formattingNumber.
  const 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) {
  'use strict';
  // Handle infinity.
  if (!isFinite(number)) {
    return number > 0 ? number : 0;
  }
  // Turns out Math.log(1000000)/Math.LN10 is strictly less than 6.
  // TODO(nickreid): Make this use `decimalShift_` or use another more efficient
  // string-based method.
  let i = 0;
  while ((number /= 10) >= 1) i++;
  return i;
};

/**
 * Shifts `number` by `digitCount` decimal digits.
 *
 * This function corrects for rounding error that may occur when naively
 * multiplying or dividing by a power of 10. See:
 * https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems
 * Example: `1.1e27 / Math.pow(10, 12)  != 1.1e15`.
 *
 * This function does not correct for inherent limitations in the precision of
 * JavaScript numbers.
 *
 * @param {number} number The number to shift.
 * @param {number} digitCount The number of places by which to shift number.
 *     Must be an integer. May be positive or negative.
 * @return {number}
 * @private
 */
goog.i18n.NumberFormat.decimalShift_ = function(number, digitCount) {
  'use strict';
  goog.asserts.assert(
      digitCount % 1 == 0, 'Cannot shift by fractional digits "%s".',
      digitCount);

  // Make sure to cover all numbers that stringify to something that doesn't
  // look like a number.
  if (!number || !isFinite(number) || digitCount == 0) {
    return number;
  }

  // This method isn't efficient, but it has the exact behaviour we want without
  // worrying about floating-point math edge cases.
  const numParts = String(number).split('e');
  const magnitude = parseInt(numParts[1] || 0, 10) + digitCount;
  return parseFloat(numParts[0] + 'e' + magnitude);
};

/**
 * Rounds `number` to `decimalCount` decimal places.
 *
 * Negative values of `decimalCount` will eliminate integeral digits.
 *
 * This function corrects for rounding error that may occur when naively
 * multiplying by a power of 10.
 *
 * This function does not correct for inherent limitations in the precision of
 * JavaScript numbers.
 *
 * @param {number} number The number to round.
 * @param {number} decimalCount The number of decimal places to retain.
 *     Must be an integer. May be positive or negative.
 * @return {number}
 * @private
 */
goog.i18n.NumberFormat.decimalRound_ = function(number, decimalCount) {
  'use strict';
  goog.asserts.assert(
      decimalCount % 1 == 0, 'Cannot round to fractional digits "%s".',
      decimalCount);

  if (!number || !isFinite(number)) {
    return number;
  }

  const shift = goog.i18n.NumberFormat.decimalShift_;
  return shift(Math.round(shift(number, decimalCount)), -decimalCount);
};


/**
 * 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) {
  'use strict';
  if (!number) return number;

  const digits = this.intLog10_(number);
  const magnitude = significantDigits - digits - 1;

  // Only round fraction, not (potentially shifted) integers.
  if (magnitude < -scale) {
    return goog.i18n.NumberFormat.decimalRound_(number, -scale);
  } else {
    return goog.i18n.NumberFormat.decimalRound_(number, magnitude);
  }
};


/**
 * 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) {
  'use strict';
  /* 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() {
  'use strict';
  if (goog.i18n.NumberFormat.USE_ECMASCRIPT_I18N_NUMFORMAT &&
      this.intlFormatter_) {
    // Examine the part of the output, checking if currency position preceeds
    // numbers.

    /** @type {!Array<!goog.i18n.NumberFormat.FormattedPart>} */
    const resultParts = this.intlFormatter_.formatToParts(1000);

    let partIndex = 0;
    // Stop looking when either the currency or number is found.
    while (partIndex < resultParts.length) {
      const partType = resultParts[partIndex]['type'];
      if (partType == 'currency') {
        return true;  // If we see currency before a number.
      }
      if ((partType == 'integer') || (partType == 'decimal')) {
        return false;
      }
      partIndex++;
    }
    return false;  // Nothing found
  }
  const posCurrSymbol = this.pattern_.indexOf('\u00A4');  // 'ยค' Currency sign
  const posPound = this.pattern_.indexOf('#');
  const 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.
  let 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;
};