| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| part of intl; |
| |
| /// The function that we pass internally to NumberFormat to get |
| /// the appropriate pattern (e.g. currency) |
| typedef String _PatternGetter(NumberSymbols symbols); |
| |
| /// Provides the ability to format a number in a locale-specific way. The |
| /// format is specified as a pattern using a subset of the ICU formatting |
| /// patterns. |
| /// |
| /// - `0` A single digit |
| /// - `#` A single digit, omitted if the value is zero |
| /// - `.` Decimal separator |
| /// - `-` Minus sign |
| /// - `,` Grouping separator |
| /// - `E` Separates mantissa and expontent |
| /// - `+` - Before an exponent, to say it should be prefixed with a plus sign. |
| /// - `%` - In prefix or suffix, multiply by 100 and show as percentage |
| /// - `‰ (\u2030)` In prefix or suffix, multiply by 1000 and show as per mille |
| /// - `¤ (\u00A4)` Currency sign, replaced by currency name |
| /// - `'` Used to quote special characters |
| /// - `;` Used to separate the positive and negative patterns (if both present) |
| /// |
| /// For example, |
| /// var f = new NumberFormat("###.0#", "en_US"); |
| /// print(f.format(12.345)); |
| /// ==> 12.34 |
| /// If the locale is not specified, it will default to the current locale. If |
| /// the format is not specified it will print in a basic format with at least |
| /// one integer digit and three fraction digits. |
| /// |
| /// There are also standard patterns available via the special constructors. |
| /// e.g. |
| /// var percent = new NumberFormat.percentFormat("ar"); |
| /// var eurosInUSFormat = new NumberFormat.currency(locale: "en_US", |
| /// symbol: "€"); |
| /// There are four such constructors: decimalFormat, percentFormat, |
| /// scientificFormat and currencyFormat. However, at the moment, |
| /// scientificFormat prints only as equivalent to "#E0" and does not take |
| /// into account significant digits. The currencyFormat will default to the |
| /// three-letter name of the currency if no explicit name/symbol is provided. |
| class NumberFormat { |
| /// Variables to determine how number printing behaves. |
| // TODO(alanknight): If these remain as variables and are set based on the |
| // pattern, can we make them final? |
| String _negativePrefix = '-'; |
| String _positivePrefix = ''; |
| String _negativeSuffix = ''; |
| String _positiveSuffix = ''; |
| |
| /// How many numbers in a group when using punctuation to group digits in |
| /// large numbers. e.g. in en_US: "1,000,000" has a grouping size of 3 digits |
| /// between commas. |
| int _groupingSize = 3; |
| |
| /// In some formats the last grouping size may be different than previous |
| /// ones, e.g. Hindi. |
| int _finalGroupingSize = 3; |
| |
| /// Set to true if the format has explicitly set the grouping size. |
| bool _groupingSizeSetExplicitly = false; |
| bool _decimalSeparatorAlwaysShown = false; |
| bool _useSignForPositiveExponent = false; |
| bool _useExponentialNotation = false; |
| |
| /// Explicitly store if we are a currency format, and so should use the |
| /// appropriate number of decimal digits for a currency. |
| // TODO(alanknight): Handle currency formats which are specified in a raw |
| /// pattern, not using one of the currency constructors. |
| bool _isForCurrency = false; |
| |
| int maximumIntegerDigits = 40; |
| int minimumIntegerDigits = 1; |
| int maximumFractionDigits = 3; |
| int minimumFractionDigits = 0; |
| int minimumExponentDigits = 0; |
| int _significantDigits = 0; |
| |
| /// How many significant digits should we print. |
| /// |
| /// Note that if significantDigitsInUse is the default false, this |
| /// will be ignored. |
| int get significantDigits => _significantDigits; |
| set significantDigits(int x) { |
| _significantDigits = x; |
| significantDigitsInUse = true; |
| } |
| |
| bool significantDigitsInUse = false; |
| |
| /// For percent and permille, what are we multiplying by in order to |
| /// get the printed value, e.g. 100 for percent. |
| int get _multiplier => _internalMultiplier; |
| set _multiplier(int x) { |
| _internalMultiplier = x; |
| _multiplierDigits = (log(_multiplier) / LN10).round(); |
| } |
| |
| int _internalMultiplier = 1; |
| |
| /// How many digits are there in the [_multiplier]. |
| int _multiplierDigits = 0; |
| |
| /// Stores the pattern used to create this format. This isn't used, but |
| /// is helpful in debugging. |
| String _pattern; |
| |
| /// The locale in which we print numbers. |
| final String _locale; |
| |
| /// Caches the symbols used for our locale. |
| NumberSymbols _symbols; |
| |
| /// The name of the currency to print, in ISO 4217 form. |
| String currencyName; |
| |
| /// The symbol to be used when formatting this as currency. |
| /// |
| /// For example, "$", "US$", or "€". |
| String _currencySymbol; |
| |
| /// The symbol to be used when formatting this as currency. |
| /// |
| /// For example, "$", "US$", or "€". |
| String get currencySymbol => _currencySymbol ?? currencyName; |
| |
| /// The number of decimal places to use when formatting. |
| /// |
| /// If this is not explicitly specified in the constructor, then for |
| /// currencies we use the default value for the currency if the name is given, |
| /// otherwise we use the value from the pattern for the locale. |
| /// |
| /// So, for example, |
| /// new NumberFormat.currency(name: 'USD', decimalDigits: 7) |
| /// will format with 7 decimal digits, because that's what we asked for. But |
| /// new NumberFormat.currency(locale: 'en_US', name: 'JPY') |
| /// will format with zero, because that's the default for JPY, and the |
| /// currency's default takes priority over the locale's default. |
| /// new NumberFormat.currency(locale: 'en_US') |
| /// will format with two, which is the default for that locale. |
| /// |
| int get decimalDigits => _decimalDigits; |
| |
| int _decimalDigits; |
| |
| /// For currencies, the default number of decimal places to use in |
| /// formatting. Defaults to two for non-currencies or currencies where it's |
| /// not specified. |
| int get _defaultDecimalDigits => |
| currencyFractionDigits[currencyName.toUpperCase()] ?? |
| currencyFractionDigits['DEFAULT']; |
| |
| /// If we have a currencyName, use the decimal digits for that currency, |
| /// unless we've explicitly specified some other number. |
| bool get _overridesDecimalDigits => decimalDigits != null || _isForCurrency; |
| |
| /// Transient internal state in which to build up the result of the format |
| /// operation. We can have this be just an instance variable because Dart is |
| /// single-threaded and unless we do an asynchronous operation in the process |
| /// of formatting then there will only ever be one number being formatted |
| /// at a time. In languages with threads we'd need to pass this on the stack. |
| final StringBuffer _buffer = new StringBuffer(); |
| |
| /// Create a number format that prints using [newPattern] as it applies in |
| /// [locale]. |
| factory NumberFormat([String newPattern, String locale]) => |
| new NumberFormat._forPattern(locale, (x) => newPattern); |
| |
| /// Create a number format that prints as DECIMAL_PATTERN. |
| NumberFormat.decimalPattern([String locale]) |
| : this._forPattern(locale, (x) => x.DECIMAL_PATTERN); |
| |
| /// Create a number format that prints as PERCENT_PATTERN. |
| NumberFormat.percentPattern([String locale]) |
| : this._forPattern(locale, (x) => x.PERCENT_PATTERN); |
| |
| /// Create a number format that prints as SCIENTIFIC_PATTERN. |
| NumberFormat.scientificPattern([String locale]) |
| : this._forPattern(locale, (x) => x.SCIENTIFIC_PATTERN); |
| |
| /// A regular expression to validate currency names are exactly three |
| /// alphabetic characters. |
| static final _checkCurrencyName = new RegExp(r'^[a-zA-Z]{3}$'); |
| |
| /// Create a number format that prints as CURRENCY_PATTERN. (Deprecated: |
| /// prefer NumberFormat.currency) |
| /// |
| /// If provided, |
| /// use [nameOrSymbol] in place of the default currency name. e.g. |
| /// var eurosInCurrentLocale = new NumberFormat |
| /// .currencyPattern(Intl.defaultLocale, "€"); |
| @Deprecated("Use NumberFormat.currency") |
| factory NumberFormat.currencyPattern( |
| [String locale, String currencyNameOrSymbol]) { |
| // If it looks like an iso4217 name, pass as name, otherwise as symbol. |
| if (currencyNameOrSymbol != null && |
| _checkCurrencyName.hasMatch(currencyNameOrSymbol)) { |
| return new NumberFormat.currency( |
| locale: locale, name: currencyNameOrSymbol); |
| } else { |
| return new NumberFormat.currency( |
| locale: locale, symbol: currencyNameOrSymbol); |
| } |
| } |
| |
| /// Create a [NumberFormat] that formats using the locale's CURRENCY_PATTERN. |
| /// |
| /// If [locale] is not specified, it will use the current default locale. |
| /// |
| /// If [name] is specified, the currency with that ISO 4217 name will be used. |
| /// Otherwise we will use the default currency name for the current locale. If |
| /// no [symbol] is specified, we will use the currency name in the formatted |
| /// result. e.g. |
| /// var f = new NumberFormat.currency(locale: 'en_US', name: 'EUR') |
| /// will format currency like "EUR1.23". If we did not specify the name, it |
| /// would format like "USD1.23". |
| /// |
| /// If [symbol] is used, then that symbol will be used in formatting instead |
| /// of the name. e.g. |
| /// var eurosInCurrentLocale = new NumberFormat.currency(symbol: "€"); |
| /// will format like "€1.23". Otherwise it will use the currency name. |
| /// If this is not explicitly specified in the constructor, then for |
| /// currencies we use the default value for the currency if the name is given, |
| /// otherwise we use the value from the pattern for the locale. |
| /// |
| /// If [decimalDigits] is specified, numbers will format with that many digits |
| /// after the decimal place. If it's not, they will use the default for the |
| /// currency in [name], and the default currency for [locale] if the currency |
| /// name is not specified. e.g. |
| /// new NumberFormat.currency(name: 'USD', decimalDigits: 7) |
| /// will format with 7 decimal digits, because that's what we asked for. But |
| /// new NumberFormat.currency(locale: 'en_US', name: 'JPY') |
| /// will format with zero, because that's the default for JPY, and the |
| /// currency's default takes priority over the locale's default. |
| /// new NumberFormat.currency(locale: 'en_US') |
| /// will format with two, which is the default for that locale. |
| // TODO(alanknight): Should we allow decimalDigits on other numbers. |
| NumberFormat.currency( |
| {String locale, String name, String symbol, int decimalDigits}) |
| : this._forPattern(locale, (x) => x.CURRENCY_PATTERN, |
| name: name, |
| currencySymbol: symbol, |
| decimalDigits: decimalDigits, |
| isForCurrency: true); |
| |
| /// Creates a [NumberFormat] for currencies, using the simple symbol for the |
| /// currency if one is available (e.g. $, €), so it should only be used if the |
| /// short currency symbol will be unambiguous. |
| /// |
| /// If [locale] is not specified, it will use the current default locale. |
| /// |
| /// If [name] is specified, the currency with that ISO 4217 name will be used. |
| /// Otherwise we will use the default currency name for the current locale. We |
| /// will assume that the symbol for this is well known in the locale and |
| /// unambiguous. If you format CAD in an en_US locale using this format it |
| /// will display as "$", which may be confusing to the user. |
| /// |
| /// If [decimalDigits] is specified, numbers will format with that many digits |
| /// after the decimal place. If it's not, they will use the default for the |
| /// currency in [name], and the default currency for [locale] if the currency |
| /// name is not specified. e.g. |
| /// new NumberFormat.simpleCurrency(name: 'USD', decimalDigits: 7) |
| /// will format with 7 decimal digits, because that's what we asked for. But |
| /// new NumberFormat.simpleCurrency(locale: 'en_US', name: 'JPY') |
| /// will format with zero, because that's the default for JPY, and the |
| /// currency's default takes priority over the locale's default. |
| /// new NumberFormat.simpleCurrency(locale: 'en_US') |
| /// will format with two, which is the default for that locale. |
| factory NumberFormat.simpleCurrency( |
| {String locale, String name, int decimalDigits}) { |
| return new NumberFormat._forPattern(locale, (x) => x.CURRENCY_PATTERN, |
| name: name, |
| computeCurrencySymbol: (format) => |
| _simpleCurrencySymbols[format.currencyName] ?? format.currencyName, |
| decimalDigits: decimalDigits, |
| isForCurrency: true); |
| } |
| |
| /// Returns the simple currency symbol for given currency code, or |
| /// [currencyCode] if no simple symbol is listed. |
| /// |
| /// The simple currency symbol is generally short, and the same or related to |
| /// what is used in countries having the currency as an official symbol. It |
| /// may be a symbol character, or may have letters, or both. It may be |
| /// different according to the locale: for example, for an Arabic locale it |
| /// may consist of Arabic letters, but for a French locale consist of Latin |
| /// letters. It will not be unique: for example, "$" can appear for both USD |
| /// and CAD. |
| /// |
| /// (The current implementation is the same for all locales, but this is |
| /// temporary and callers shouldn't rely on it.) |
| String simpleCurrencySymbol(String currencyCode) => |
| _simpleCurrencySymbols[currencyCode] ?? currencyCode; |
| |
| /// A map from currency names to the simple name/symbol. |
| /// |
| /// The simple currency symbol is generally short, and the same or related to |
| /// what is used in countries having the currency as an official symbol. It |
| /// may be a symbol character, or may have letters, or both. It may be |
| /// different according to the locale: for example, for an Arabic locale it |
| /// may consist of Arabic letters, but for a French locale consist of Latin |
| /// letters. It will not be unique: for example, "$" can appear for both USD |
| /// and CAD. |
| /// |
| /// (The current implementation is the same for all locales, but this is |
| /// temporary and callers shouldn't rely on it.) |
| static Map<String, String> _simpleCurrencySymbols = { |
| "AFN": "Af.", |
| "TOP": r"T$", |
| "MGA": "Ar", |
| "THB": "\u0e3f", |
| "PAB": "B/.", |
| "ETB": "Birr", |
| "VEF": "Bs", |
| "BOB": "Bs", |
| "GHS": "GHS", |
| "CRC": "\u20a1", |
| "NIO": r"C$", |
| "GMD": "GMD", |
| "MKD": "din", |
| "BHD": "din", |
| "DZD": "din", |
| "IQD": "din", |
| "JOD": "din", |
| "KWD": "din", |
| "LYD": "din", |
| "RSD": "din", |
| "TND": "din", |
| "AED": "dh", |
| "MAD": "dh", |
| "STD": "Db", |
| "BSD": r"$", |
| "FJD": r"$", |
| "GYD": r"$", |
| "KYD": r"$", |
| "LRD": r"$", |
| "SBD": r"$", |
| "SRD": r"$", |
| "AUD": r"$", |
| "BBD": r"$", |
| "BMD": r"$", |
| "BND": r"$", |
| "BZD": r"$", |
| "CAD": r"$", |
| "HKD": r"$", |
| "JMD": r"$", |
| "NAD": r"$", |
| "NZD": r"$", |
| "SGD": r"$", |
| "TTD": r"$", |
| "TWD": r"NT$", |
| "USD": r"$", |
| "XCD": r"$", |
| "VND": "\u20ab", |
| "AMD": "Dram", |
| "CVE": "CVE", |
| "EUR": "\u20ac", |
| "AWG": "Afl.", |
| "HUF": "Ft", |
| "BIF": "FBu", |
| "CDF": "FrCD", |
| "CHF": "CHF", |
| "DJF": "Fdj", |
| "GNF": "FG", |
| "RWF": "RF", |
| "XOF": "CFA", |
| "XPF": "FCFP", |
| "KMF": "CF", |
| "XAF": "FCFA", |
| "HTG": "HTG", |
| "PYG": "Gs", |
| "UAH": "\u20b4", |
| "PGK": "PGK", |
| "LAK": "\u20ad", |
| "CZK": "K\u010d", |
| "SEK": "kr", |
| "ISK": "kr", |
| "DKK": "kr", |
| "NOK": "kr", |
| "HRK": "kn", |
| "MWK": "MWK", |
| "ZMK": "ZWK", |
| "AOA": "Kz", |
| "MMK": "K", |
| "GEL": "GEL", |
| "LVL": "Ls", |
| "ALL": "Lek", |
| "HNL": "L", |
| "SLL": "SLL", |
| "MDL": "MDL", |
| "RON": "RON", |
| "BGN": "lev", |
| "SZL": "SZL", |
| "TRY": "TL", |
| "LTL": "Lt", |
| "LSL": "LSL", |
| "AZN": "man.", |
| "BAM": "KM", |
| "MZN": "MTn", |
| "NGN": "\u20a6", |
| "ERN": "Nfk", |
| "BTN": "Nu.", |
| "MRO": "MRO", |
| "MOP": "MOP", |
| "CUP": r"$", |
| "CUC": r"$", |
| "ARS": r"$", |
| "CLF": "UF", |
| "CLP": r"$", |
| "COP": r"$", |
| "DOP": r"$", |
| "MXN": r"$", |
| "PHP": "\u20b1", |
| "UYU": r"$", |
| "FKP": "£", |
| "GIP": "£", |
| "SHP": "£", |
| "EGP": "E£", |
| "LBP": "L£", |
| "SDG": "SDG", |
| "SSP": "SSP", |
| "GBP": "£", |
| "SYP": "£", |
| "BWP": "P", |
| "GTQ": "Q", |
| "ZAR": "R", |
| "BRL": r"R$", |
| "OMR": "Rial", |
| "QAR": "Rial", |
| "YER": "Rial", |
| "IRR": "Rial", |
| "KHR": "Riel", |
| "MYR": "RM", |
| "SAR": "Rial", |
| "BYR": "BYR", |
| "RUB": "руб.", |
| "MUR": "Rs", |
| "SCR": "SCR", |
| "LKR": "Rs", |
| "NPR": "Rs", |
| "INR": "\u20b9", |
| "PKR": "Rs", |
| "IDR": "Rp", |
| "ILS": "\u20aa", |
| "KES": "Ksh", |
| "SOS": "SOS", |
| "TZS": "TSh", |
| "UGX": "UGX", |
| "PEN": "S/.", |
| "KGS": "KGS", |
| "UZS": "so\u02bcm", |
| "TJS": "Som", |
| "BDT": "\u09f3", |
| "WST": "WST", |
| "KZT": "\u20b8", |
| "MNT": "\u20ae", |
| "VUV": "VUV", |
| "KPW": "\u20a9", |
| "KRW": "\u20a9", |
| "JPY": "¥", |
| "CNY": "¥", |
| "PLN": "z\u0142", |
| "MVR": "Rf", |
| "NLG": "NAf", |
| "ZMW": "ZK", |
| "ANG": "ƒ", |
| "TMT": "TMT", |
| }; |
| |
| /// Create a number format that prints in a pattern we get from |
| /// the [getPattern] function using the locale [locale]. |
| /// |
| /// The [currencySymbol] can either be specified directly, or we can pass a |
| /// function [computeCurrencySymbol] that will compute it later, given other |
| /// information, typically the verified locale. |
| NumberFormat._forPattern(String locale, _PatternGetter getPattern, |
| {String name, |
| String currencySymbol, |
| String computeCurrencySymbol(NumberFormat), |
| int decimalDigits, |
| bool isForCurrency: false}) |
| : _locale = Intl.verifiedLocale(locale, localeExists), |
| _isForCurrency = isForCurrency { |
| this._currencySymbol = currencySymbol; |
| this._decimalDigits = decimalDigits; |
| _symbols = numberFormatSymbols[_locale]; |
| currencyName = name ?? _symbols.DEF_CURRENCY_CODE; |
| if (this._currencySymbol == null && computeCurrencySymbol != null) { |
| this._currencySymbol = computeCurrencySymbol(this); |
| } |
| _setPattern(getPattern(_symbols)); |
| } |
| |
| /// A number format for compact representations, e.g. "1.2M" instead |
| /// of "1,200,000". |
| factory NumberFormat.compact({String locale}) { |
| return new _CompactNumberFormat( |
| locale: locale, |
| formatType: _CompactFormatType.COMPACT_DECIMAL_SHORT_PATTERN); |
| } |
| |
| /// A number format for "long" compact representations, e.g. "1.2 million" |
| /// instead of of "1,200,000". |
| factory NumberFormat.compactLong({String locale}) { |
| return new _CompactNumberFormat( |
| locale: locale, |
| formatType: _CompactFormatType.COMPACT_DECIMAL_LONG_PATTERN); |
| } |
| |
| /// A number format for compact currency representations, e.g. "$1.2M" instead |
| /// of "$1,200,000", and which will automatically determine a currency symbol |
| /// based on the currency name or the locale. See |
| /// [NumberFormat.simpleCurrency]. |
| factory NumberFormat.compactSimpleCurrency({String locale, String name}) { |
| return new _CompactNumberFormat( |
| locale: locale, |
| formatType: _CompactFormatType.COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN, |
| name: name, |
| getPattern: (symbols) => symbols.CURRENCY_PATTERN, |
| computeCurrencySymbol: (format) => |
| _simpleCurrencySymbols[format.currencyName] ?? format.currencyName, |
| isForCurrency: true); |
| } |
| |
| /// A number format for compact currency representations, e.g. "$1.2M" instead |
| /// of "$1,200,000". |
| factory NumberFormat.compactCurrency( |
| {String locale, String name, String symbol, int decimalDigits}) { |
| return new _CompactNumberFormat( |
| locale: locale, |
| formatType: _CompactFormatType.COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN, |
| name: name, |
| decimalDigits: decimalDigits, |
| isForCurrency: true); |
| } |
| |
| /// Return the locale code in which we operate, e.g. 'en_US' or 'pt'. |
| String get locale => _locale; |
| |
| /// Return true if the locale exists, or if it is null. The null case |
| /// is interpreted to mean that we use the default locale. |
| static bool localeExists(localeName) { |
| if (localeName == null) return false; |
| return numberFormatSymbols.containsKey(localeName); |
| } |
| |
| /// Return the symbols which are used in our locale. Cache them to avoid |
| /// repeated lookup. |
| NumberSymbols get symbols => _symbols; |
| |
| /// Format [number] according to our pattern and return the formatted string. |
| String format(number) { |
| if (_isNaN(number)) return symbols.NAN; |
| if (_isInfinite(number)) return "${_signPrefix(number)}${symbols.INFINITY}"; |
| |
| _add(_signPrefix(number)); |
| _formatNumber(number.abs()); |
| _add(_signSuffix(number)); |
| |
| var result = _buffer.toString(); |
| _buffer.clear(); |
| return result; |
| } |
| |
| /// Parse the number represented by the string. If it's not |
| /// parseable, throws a [FormatException]. |
| num parse(String text) => new _NumberParser(this, text).value; |
| |
| /// Format the main part of the number in the form dictated by the pattern. |
| void _formatNumber(number) { |
| if (_useExponentialNotation) { |
| _formatExponential(number); |
| } else { |
| _formatFixed(number); |
| } |
| } |
| |
| /// Format the number in exponential notation. |
| void _formatExponential(num number) { |
| if (number == 0.0) { |
| _formatFixed(number); |
| _formatExponent(0); |
| return; |
| } |
| |
| var exponent = (log(number) / LN10).floor(); |
| var mantissa = number / pow(10.0, exponent); |
| |
| if (maximumIntegerDigits > 1 && |
| maximumIntegerDigits > 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 % maximumIntegerDigits) != 0) { |
| mantissa *= 10; |
| exponent--; |
| } |
| } else { |
| // No repeating range is defined, use minimum integer digits. |
| if (minimumIntegerDigits < 1) { |
| exponent++; |
| mantissa /= 10; |
| } else { |
| exponent -= minimumIntegerDigits - 1; |
| mantissa *= pow(10, minimumIntegerDigits - 1); |
| } |
| } |
| _formatFixed(mantissa); |
| _formatExponent(exponent); |
| } |
| |
| /// Format the exponent portion, e.g. in "1.3e-5" the "e-5". |
| void _formatExponent(num exponent) { |
| _add(symbols.EXP_SYMBOL); |
| if (exponent < 0) { |
| exponent = -exponent; |
| _add(symbols.MINUS_SIGN); |
| } else if (_useSignForPositiveExponent) { |
| _add(symbols.PLUS_SIGN); |
| } |
| _pad(minimumExponentDigits, exponent.toString()); |
| } |
| |
| /// Used to test if we have exceeded Javascript integer limits. |
| final _maxInt = pow(2, 52); |
| |
| /// Helpers to check numbers that don't conform to the [num] interface, |
| /// e.g. Int64 |
| _isInfinite(number) => number is num ? number.isInfinite : false; |
| _isNaN(number) => number is num ? number.isNaN : false; |
| |
| /// Helper to get the floor of a number which might not be num. This should |
| /// only ever be called with an argument which is positive, or whose abs() |
| /// is negative. The second case is the maximum negative value on a |
| /// fixed-length integer. Since they are integers, they are also their own |
| /// floor. |
| _floor(number) { |
| if (number.isNegative && !(number.abs().isNegative)) { |
| throw new ArgumentError( |
| "Internal error: expected positive number, got $number"); |
| } |
| return (number is num) ? number.floor() : number ~/ 1; |
| } |
| |
| /// Helper to round a number which might not be num. |
| _round(number) { |
| if (number is num) { |
| if (number.isInfinite) { |
| return _maxInt; |
| } else { |
| return number.round(); |
| } |
| } else if (number.remainder(1) == 0) { |
| // Not a normal number, but int-like, e.g. Int64 |
| return number; |
| } else { |
| // TODO(alanknight): Do this more efficiently. If IntX had floor and round we could avoid this. |
| var basic = _floor(number); |
| var fraction = (number - basic).toDouble().round(); |
| return fraction == 0 ? number : number + fraction; |
| } |
| } |
| |
| // Return the number of digits left of the decimal place in [number]. |
| static int numberOfIntegerDigits(number) { |
| var simpleNumber = number.toDouble().abs(); |
| // It's unfortunate that we have to do this, but we get precision errors |
| // that affect the result if we use logs, e.g. 1000000 |
| if (simpleNumber < 10) return 1; |
| if (simpleNumber < 100) return 2; |
| if (simpleNumber < 1000) return 3; |
| if (simpleNumber < 10000) return 4; |
| if (simpleNumber < 100000) return 5; |
| if (simpleNumber < 1000000) return 6; |
| if (simpleNumber < 10000000) return 7; |
| if (simpleNumber < 100000000) return 8; |
| if (simpleNumber < 1000000000) return 9; |
| if (simpleNumber < 10000000000) return 10; |
| if (simpleNumber < 100000000000) return 11; |
| if (simpleNumber < 1000000000000) return 12; |
| if (simpleNumber < 10000000000000) return 13; |
| if (simpleNumber < 100000000000000) return 14; |
| if (simpleNumber < 1000000000000000) return 15; |
| if (simpleNumber < 10000000000000000) return 16; |
| // We're past the point where being off by one on the number of digits |
| // will affect the pattern, so now we can use logs. |
| return max(1, (log(simpleNumber) / LN10).ceil()); |
| } |
| |
| int _fractionDigitsAfter(int remainingSignificantDigits) => |
| max(0, remainingSignificantDigits); |
| |
| /// Format the basic number portion, including the fractional digits. |
| void _formatFixed(number) { |
| var integerPart; |
| int fractionPart; |
| int extraIntegerDigits; |
| var fractionDigits = maximumFractionDigits; |
| |
| var power = 0; |
| var digitMultiplier; |
| |
| if (_isInfinite(number)) { |
| integerPart = number.toInt(); |
| extraIntegerDigits = 0; |
| fractionPart = 0; |
| } else { |
| // We have three possible pieces. First, the basic integer part. If this |
| // is a percent or permille, the additional 2 or 3 digits. Finally the |
| // fractional part. |
| // We avoid multiplying the number because it might overflow if we have |
| // a fixed-size integer type, so we extract each of the three as an |
| // integer pieces. |
| integerPart = _floor(number); |
| var fraction = number - integerPart; |
| |
| /// If we have significant digits, recalculate the number of fraction |
| /// digits based on that. |
| if (significantDigitsInUse) { |
| var integerLength = numberOfIntegerDigits(integerPart); |
| var remainingSignificantDigits = |
| significantDigits - _multiplierDigits - integerLength; |
| fractionDigits = _fractionDigitsAfter(remainingSignificantDigits); |
| if (remainingSignificantDigits < 0) { |
| // We may have to round. |
| var divideBy = pow(10, integerLength - significantDigits); |
| integerPart = (integerPart / divideBy).round() * divideBy; |
| } |
| } |
| power = pow(10, fractionDigits); |
| digitMultiplier = power * _multiplier; |
| |
| // Multiply out to the number of decimal places and the percent, then |
| // round. For fixed-size integer types this should always be zero, so |
| // multiplying is OK. |
| var remainingDigits = _round(fraction * digitMultiplier).toInt(); |
| // However, in rounding we may overflow into the main digits. |
| if (remainingDigits >= digitMultiplier) { |
| integerPart++; |
| remainingDigits -= digitMultiplier; |
| } |
| // Separate out the extra integer parts from the fraction part. |
| extraIntegerDigits = remainingDigits ~/ power; |
| fractionPart = remainingDigits % power; |
| } |
| |
| var integerDigits = _integerDigits(integerPart, extraIntegerDigits); |
| var digitLength = integerDigits.length; |
| var fractionPresent = |
| fractionDigits > 0 && (minimumFractionDigits > 0 || fractionPart > 0); |
| |
| if (_hasIntegerDigits(integerDigits)) { |
| _pad(minimumIntegerDigits - digitLength); |
| for (var i = 0; i < digitLength; i++) { |
| _addDigit(integerDigits.codeUnitAt(i)); |
| _group(digitLength, i); |
| } |
| } else if (!fractionPresent) { |
| // If neither fraction nor integer part exists, just print zero. |
| _addZero(); |
| } |
| |
| _decimalSeparator(fractionPresent); |
| _formatFractionPart((fractionPart + power).toString()); |
| } |
| |
| /// Compute the raw integer digits which will then be printed with |
| /// grouping and translated to localized digits. |
| String _integerDigits(integerPart, extraIntegerDigits) { |
| // If the int part is larger than 2^52 and we're on Javascript (so it's |
| // really a float) it will lose precision, so pad out the rest of it |
| // with zeros. Check for Javascript by seeing if an integer is double. |
| var paddingDigits = ''; |
| if (1 is double && integerPart is num && integerPart > _maxInt) { |
| var howManyDigitsTooBig = (log(integerPart) / LN10).ceil() - 16; |
| var divisor = pow(10, howManyDigitsTooBig).round(); |
| paddingDigits = symbols.ZERO_DIGIT * howManyDigitsTooBig.toInt(); |
| integerPart = (integerPart / divisor).truncate(); |
| } |
| |
| var extra = extraIntegerDigits == 0 ? '' : extraIntegerDigits.toString(); |
| var intDigits = _mainIntegerDigits(integerPart); |
| var paddedExtra = |
| intDigits.isEmpty ? extra : extra.padLeft(_multiplierDigits, '0'); |
| return "${intDigits}${paddedExtra}${paddingDigits}"; |
| } |
| |
| /// The digit string of the integer part. This is the empty string if the |
| /// integer part is zero and otherwise is the toString() of the integer |
| /// part, stripping off any minus sign. |
| String _mainIntegerDigits(integer) { |
| if (integer == 0) return ''; |
| var digits = integer.toString(); |
| if (significantDigitsInUse && digits.length > significantDigits) { |
| digits = digits.substring(0, significantDigits) + |
| ''.padLeft(digits.length - significantDigits, '0'); |
| } |
| // If we have a fixed-length int representation, it can have a negative |
| // number whose negation is also negative, e.g. 2^-63 in 64-bit. |
| // Remove the minus sign. |
| return digits.startsWith('-') ? digits.substring(1) : digits; |
| } |
| |
| /// Format the part after the decimal place in a fixed point number. |
| void _formatFractionPart(String fractionPart) { |
| var fractionCodes = fractionPart.codeUnits; |
| var fractionLength = fractionPart.length; |
| while (fractionCodes[fractionLength - 1] == _zero && |
| fractionLength > minimumFractionDigits + 1) { |
| fractionLength--; |
| } |
| for (var i = 1; i < fractionLength; i++) { |
| _addDigit(fractionCodes[i]); |
| } |
| } |
| |
| /// Print the decimal separator if appropriate. |
| void _decimalSeparator(bool fractionPresent) { |
| if (_decimalSeparatorAlwaysShown || fractionPresent) { |
| _add(symbols.DECIMAL_SEP); |
| } |
| } |
| |
| /// Return true if we have a main integer part which is printable, either |
| /// because we have digits left of the decimal point (this may include digits |
| /// which have been moved left because of percent or permille formatting), |
| /// or because the minimum number of printable digits is greater than 1. |
| bool _hasIntegerDigits(String digits) => |
| digits.isNotEmpty || minimumIntegerDigits > 0; |
| |
| /// A group of methods that provide support for writing digits and other |
| /// required characters into [_buffer] easily. |
| void _add(String x) { |
| _buffer.write(x); |
| } |
| |
| void _addZero() { |
| _buffer.write(symbols.ZERO_DIGIT); |
| } |
| |
| void _addDigit(int x) { |
| _buffer.writeCharCode(_localeZero + x - _zero); |
| } |
| |
| /// Print padding up to [numberOfDigits] above what's included in [basic]. |
| void _pad(int numberOfDigits, [String basic = '']) { |
| for (var i = 0; i < numberOfDigits - basic.length; i++) { |
| _add(symbols.ZERO_DIGIT); |
| } |
| for (int i = 0; i < basic.codeUnits.length; i++) { |
| _addDigit(basic.codeUnits[i]); |
| } |
| } |
| |
| /// We are printing the digits of the number from left to right. We may need |
| /// to print a thousands separator or other grouping character as appropriate |
| /// to the locale. So we find how many places we are from the end of the number |
| /// by subtracting our current [position] from the [totalLength] and printing |
| /// the separator character every [_groupingSize] digits, with the final |
| /// grouping possibly being of a different size, [_finalGroupingSize]. |
| void _group(int totalLength, int position) { |
| var distanceFromEnd = totalLength - position; |
| if (distanceFromEnd <= 1 || _groupingSize <= 0) return; |
| if (distanceFromEnd == _finalGroupingSize + 1) { |
| _add(symbols.GROUP_SEP); |
| } else if ((distanceFromEnd > _finalGroupingSize) && |
| (distanceFromEnd - _finalGroupingSize) % _groupingSize == 1) { |
| _add(symbols.GROUP_SEP); |
| } |
| } |
| |
| /// Returns the code point for the character '0'. |
| final _zero = '0'.codeUnits.first; |
| |
| /// Returns the code point for the locale's zero digit. |
| // Note that there is a slight risk of a locale's zero digit not fitting |
| // into a single code unit, but it seems very unlikely, and if it did, |
| // there's a pretty good chance that our assumptions about being able to do |
| // arithmetic on it would also be invalid. |
| get _localeZero => symbols.ZERO_DIGIT.codeUnits.first; |
| |
| /// Returns the prefix for [x] based on whether it's positive or negative. |
| /// In en_US this would be '' and '-' respectively. |
| String _signPrefix(x) => x.isNegative ? _negativePrefix : _positivePrefix; |
| |
| /// Returns the suffix for [x] based on wether it's positive or negative. |
| /// In en_US there are no suffixes for positive or negative. |
| String _signSuffix(x) => x.isNegative ? _negativeSuffix : _positiveSuffix; |
| |
| void _setPattern(String newPattern) { |
| if (newPattern == null) return; |
| // Make spaces non-breaking |
| _pattern = newPattern.replaceAll(' ', '\u00a0'); |
| var parser = new _NumberFormatParser( |
| this, newPattern, currencySymbol, decimalDigits); |
| parser.parse(); |
| if (_overridesDecimalDigits) { |
| _decimalDigits ??= _defaultDecimalDigits; |
| minimumFractionDigits = _decimalDigits; |
| maximumFractionDigits = _decimalDigits; |
| } |
| } |
| |
| /// Explicitly turn off any grouping (e.g. by thousands) in this format. |
| /// |
| /// This is used in compact number formatting, where we |
| /// omit the normal grouping. Best to know what you're doing if you call it. |
| void turnOffGrouping() { |
| _groupingSize = 0; |
| _finalGroupingSize = 0; |
| } |
| |
| String toString() => "NumberFormat($_locale, $_pattern)"; |
| } |
| |
| /// A one-time object for parsing a particular numeric string. One-time here |
| /// means an instance can only parse one string. This is implemented by |
| /// transforming from a locale-specific format to one that the system can parse, |
| /// then calls the system parsing methods on it. |
| class _NumberParser { |
| /// The format for which we are parsing. |
| final NumberFormat format; |
| |
| /// The text we are parsing. |
| final String text; |
| |
| /// What we use to iterate over the input text. |
| final _Stream input; |
| |
| /// The result of parsing [text] according to [format]. Automatically |
| /// populated in the constructor. |
| num value; |
| |
| /// The symbols used by our format. |
| NumberSymbols get symbols => format.symbols; |
| |
| /// Where we accumulate the normalized representation of the number. |
| final StringBuffer _normalized = new StringBuffer(); |
| |
| /// Did we see something that indicates this is, or at least might be, |
| /// a positive number. |
| bool gotPositive = false; |
| |
| /// Did we see something that indicates this is, or at least might be, |
| /// a negative number. |
| bool gotNegative = false; |
| |
| /// Did we see the required positive suffix at the end. Should |
| /// match [gotPositive]. |
| bool gotPositiveSuffix = false; |
| |
| /// Did we see the required negative suffix at the end. Should |
| /// match [gotNegative]. |
| bool gotNegativeSuffix = false; |
| |
| /// Should we stop parsing before hitting the end of the string. |
| bool done = false; |
| |
| /// Have we already skipped over any required prefixes. |
| bool prefixesSkipped = false; |
| |
| /// If the number is percent or permill, what do we divide by at the end. |
| int scale = 1; |
| |
| String get _positivePrefix => format._positivePrefix; |
| String get _negativePrefix => format._negativePrefix; |
| String get _positiveSuffix => format._positiveSuffix; |
| String get _negativeSuffix => format._negativeSuffix; |
| int get _zero => format._zero; |
| int get _localeZero => format._localeZero; |
| |
| /// Create a new [_NumberParser] on which we can call parse(). |
| _NumberParser(this.format, text) |
| : this.text = text, |
| this.input = new _Stream(text) { |
| scale = format._internalMultiplier; |
| value = parse(); |
| } |
| |
| /// The strings we might replace with functions that return the replacement |
| /// values. They are functions because we might need to check something |
| /// in the context. Note that the ordering is important here. For example, |
| /// [symbols.PERCENT] might be " %", and we must handle that before we |
| /// look at an individual space. |
| Map<String, Function> get replacements => |
| _replacements ??= _initializeReplacements(); |
| |
| Map<String, Function> _replacements; |
| |
| Map<String, Function> _initializeReplacements() => { |
| symbols.DECIMAL_SEP: () => '.', |
| symbols.EXP_SYMBOL: () => 'E', |
| symbols.GROUP_SEP: handleSpace, |
| symbols.PERCENT: () { |
| scale = _NumberFormatParser._PERCENT_SCALE; |
| return ''; |
| }, |
| symbols.PERMILL: () { |
| scale = _NumberFormatParser._PER_MILLE_SCALE; |
| return ''; |
| }, |
| ' ': handleSpace, |
| '\u00a0': handleSpace, |
| '+': () => '+', |
| '-': () => '-', |
| }; |
| |
| invalidFormat() => |
| throw new FormatException("Invalid number: ${input.contents}"); |
| |
| /// Replace a space in the number with the normalized form. If space is not |
| /// a significant character (normally grouping) then it's just invalid. If it |
| /// is the grouping character, then it's only valid if it's followed by a |
| /// digit. e.g. '$12 345.00' |
| handleSpace() => |
| groupingIsNotASpaceOrElseItIsSpaceFollowedByADigit ? '' : invalidFormat(); |
| |
| /// Determine if a space is a valid character in the number. See |
| /// [handleSpace]. |
| bool get groupingIsNotASpaceOrElseItIsSpaceFollowedByADigit { |
| if (symbols.GROUP_SEP != '\u00a0' || symbols.GROUP_SEP != ' ') return true; |
| var peeked = input.peek(symbols.GROUP_SEP.length + 1); |
| return asDigit(peeked[peeked.length - 1]) != null; |
| } |
| |
| /// Turn [char] into a number representing a digit, or null if it doesn't |
| /// represent a digit in this locale. |
| int asDigit(String char) { |
| var charCode = char.codeUnitAt(0); |
| var digitValue = charCode - _localeZero; |
| if (digitValue >= 0 && digitValue < 10) { |
| return digitValue; |
| } else { |
| return null; |
| } |
| } |
| |
| /// Check to see if the input begins with either the positive or negative |
| /// prefixes. Set the [gotPositive] and [gotNegative] variables accordingly. |
| void checkPrefixes({bool skip: false}) { |
| bool checkPrefix(String prefix, skip) { |
| var matched = prefix.isNotEmpty && input.startsWith(prefix); |
| if (skip && matched) input.read(prefix.length); |
| return matched; |
| } |
| |
| // TODO(alanknight): There's a faint possibility of a bug here where |
| // a positive prefix is followed by a negative prefix that's also a valid |
| // part of the number, but that seems very unlikely. |
| if (checkPrefix(_positivePrefix, skip)) gotPositive = true; |
| if (checkPrefix(_negativePrefix, skip)) gotNegative = true; |
| |
| // Copied from Closure. It doesn't seem to be necessary to pass the test |
| // suite, so I'm not sure it's really needed. |
| if (gotPositive && gotNegative) { |
| if (_positivePrefix.length > _negativePrefix.length) { |
| gotNegative = false; |
| } else if (_negativePrefix.length > _positivePrefix.length) { |
| gotPositive = false; |
| } |
| } |
| } |
| |
| /// If the rest of our input is either the positive or negative suffix, |
| /// set [gotPositiveSuffix] or [gotNegativeSuffix] accordingly. |
| void checkSuffixes() { |
| var remainder = input.rest(); |
| if (remainder == _positiveSuffix) gotPositiveSuffix = true; |
| if (remainder == _negativeSuffix) gotNegativeSuffix = true; |
| } |
| |
| /// We've encountered a character that's not a digit. Go through our |
| /// replacement rules looking for how to handle it. If we see something |
| /// that's not a digit and doesn't have a replacement, then we're done |
| /// and the number is probably invalid. |
| void processNonDigit() { |
| // It might just be a prefix that we haven't skipped. We don't want to |
| // skip them initially because they might also be semantically meaningful, |
| // e.g. leading %. So we allow them through the loop, but only once. |
| var foundAnInterpretation = false; |
| if (input.index == 0 && !prefixesSkipped) { |
| prefixesSkipped = true; |
| checkPrefixes(skip: true); |
| foundAnInterpretation = true; |
| } |
| |
| for (var key in replacements.keys) { |
| if (input.startsWith(key)) { |
| _normalized.write(replacements[key]()); |
| input.read(key.length); |
| return; |
| } |
| } |
| // We haven't found either of these things, this seems invalid. |
| if (!foundAnInterpretation) { |
| done = true; |
| } |
| } |
| |
| /// Parse [text] and return the resulting number. Throws [FormatException] |
| /// if we can't parse it. |
| num parse() { |
| if (text == symbols.NAN) return double.NAN; |
| if (text == "$_positivePrefix${symbols.INFINITY}$_positiveSuffix") { |
| return double.INFINITY; |
| } |
| if (text == "$_negativePrefix${symbols.INFINITY}$_negativeSuffix") { |
| return double.NEGATIVE_INFINITY; |
| } |
| |
| checkPrefixes(); |
| var parsed = parseNumber(input); |
| |
| if (gotPositive && !gotPositiveSuffix) invalidNumber(); |
| if (gotNegative && !gotNegativeSuffix) invalidNumber(); |
| if (!input.atEnd()) invalidNumber(); |
| |
| return parsed; |
| } |
| |
| /// The number is invalid, throw a [FormatException]. |
| void invalidNumber() => |
| throw new FormatException("Invalid Number: ${input.contents}"); |
| |
| /// Parse the number portion of the input, i.e. not any prefixes or suffixes, |
| /// and assuming NaN and Infinity are already handled. |
| num parseNumber(_Stream input) { |
| if (gotNegative) { |
| _normalized.write('-'); |
| } |
| while (!done && !input.atEnd()) { |
| int digit = asDigit(input.peek()); |
| if (digit != null) { |
| _normalized.writeCharCode(_zero + digit); |
| input.next(); |
| } else { |
| processNonDigit(); |
| } |
| checkSuffixes(); |
| } |
| |
| var normalizedText = _normalized.toString(); |
| num parsed = int.parse(normalizedText, onError: (message) => null); |
| if (parsed == null) parsed = double.parse(normalizedText); |
| return parsed / scale; |
| } |
| } |
| |
| /// Private class that parses the numeric formatting pattern and sets the |
| /// variables in [format] to appropriate values. Instances of this are |
| /// transient and store parsing state in instance variables, so can only be used |
| /// to parse a single pattern. |
| class _NumberFormatParser { |
| /// The special characters in the pattern language. All others are treated |
| /// as literals. |
| static const _PATTERN_SEPARATOR = ';'; |
| static const _QUOTE = "'"; |
| static const _PATTERN_DIGIT = '#'; |
| static const _PATTERN_ZERO_DIGIT = '0'; |
| static const _PATTERN_GROUPING_SEPARATOR = ','; |
| static const _PATTERN_DECIMAL_SEPARATOR = '.'; |
| static const _PATTERN_CURRENCY_SIGN = '\u00A4'; |
| static const _PATTERN_PER_MILLE = '\u2030'; |
| static const _PER_MILLE_SCALE = 1000; |
| static const _PATTERN_PERCENT = '%'; |
| static const _PERCENT_SCALE = 100; |
| static const _PATTERN_EXPONENT = 'E'; |
| static const _PATTERN_PLUS = '+'; |
| |
| /// The format whose state we are setting. |
| final NumberFormat format; |
| |
| /// The pattern we are parsing. |
| final _StringIterator pattern; |
| |
| /// We can be passed a specific currency symbol, regardless of the locale. |
| String currencySymbol; |
| |
| /// We can be given a specific number of decimal places, overriding the |
| /// default. |
| final int decimalDigits; |
| |
| /// Create a new [_NumberFormatParser] for a particular [NumberFormat] and |
| /// [input] pattern. |
| _NumberFormatParser( |
| this.format, input, this.currencySymbol, this.decimalDigits) |
| : pattern = _iterator(input) { |
| pattern.moveNext(); |
| } |
| |
| /// The [NumberSymbols] for the locale in which our [format] prints. |
| NumberSymbols get symbols => format.symbols; |
| |
| /// Parse the input pattern and set the values. |
| void parse() { |
| format._positivePrefix = _parseAffix(); |
| var trunk = _parseTrunk(); |
| format._positiveSuffix = _parseAffix(); |
| // If we have separate positive and negative patterns, now parse the |
| // the negative version. |
| if (pattern.current == _NumberFormatParser._PATTERN_SEPARATOR) { |
| pattern.moveNext(); |
| format._negativePrefix = _parseAffix(); |
| // Skip over the negative trunk, verifying that it's identical to the |
| // positive trunk. |
| for (var each in _iterable(trunk)) { |
| if (pattern.current != each && pattern.current != null) { |
| throw new FormatException( |
| "Positive and negative trunks must be the same"); |
| } |
| pattern.moveNext(); |
| } |
| format._negativeSuffix = _parseAffix(); |
| } else { |
| // If no negative affix is specified, they share the same positive affix. |
| format._negativePrefix = format._negativePrefix + format._positivePrefix; |
| format._negativeSuffix = format._positiveSuffix + format._negativeSuffix; |
| } |
| } |
| |
| /// Variable used in parsing prefixes and suffixes to keep track of |
| /// whether or not we are in a quoted region. |
| bool inQuote = false; |
| |
| /// Parse a prefix or suffix and return the prefix/suffix string. Note that |
| /// this also may modify the state of [format]. |
| String _parseAffix() { |
| var affix = new StringBuffer(); |
| inQuote = false; |
| while (parseCharacterAffix(affix) && pattern.moveNext()); |
| return affix.toString(); |
| } |
| |
| /// Parse an individual character as part of a prefix or suffix. Return true |
| /// if we should continue to look for more affix characters, and false if |
| /// we have reached the end. |
| bool parseCharacterAffix(StringBuffer affix) { |
| var ch = pattern.current; |
| if (ch == null) return false; |
| if (ch == _QUOTE) { |
| if (pattern.peek == _QUOTE) { |
| pattern.moveNext(); |
| affix.write(_QUOTE); // 'don''t' |
| } else { |
| inQuote = !inQuote; |
| } |
| return true; |
| } |
| |
| if (inQuote) { |
| affix.write(ch); |
| } else { |
| switch (ch) { |
| case _PATTERN_DIGIT: |
| case _PATTERN_ZERO_DIGIT: |
| case _PATTERN_GROUPING_SEPARATOR: |
| case _PATTERN_DECIMAL_SEPARATOR: |
| case _PATTERN_SEPARATOR: |
| return false; |
| case _PATTERN_CURRENCY_SIGN: |
| // TODO(alanknight): Handle the local/global/portable currency signs |
| affix.write(currencySymbol); |
| break; |
| case _PATTERN_PERCENT: |
| if (format._multiplier != 1 && format._multiplier != _PERCENT_SCALE) { |
| throw new FormatException('Too many percent/permill'); |
| } |
| format._multiplier = _PERCENT_SCALE; |
| affix.write(symbols.PERCENT); |
| break; |
| case _PATTERN_PER_MILLE: |
| if (format._multiplier != 1 && |
| format._multiplier != _PER_MILLE_SCALE) { |
| throw new FormatException('Too many percent/permill'); |
| } |
| format._multiplier = _PER_MILLE_SCALE; |
| affix.write(symbols.PERMILL); |
| break; |
| default: |
| affix.write(ch); |
| } |
| } |
| return true; |
| } |
| |
| /// Variables used in [_parseTrunk] and [parseTrunkCharacter]. |
| var decimalPos = -1; |
| var digitLeftCount = 0; |
| var zeroDigitCount = 0; |
| var digitRightCount = 0; |
| var groupingCount = -1; |
| |
| /// Parse the "trunk" portion of the pattern, the piece that doesn't include |
| /// positive or negative prefixes or suffixes. |
| String _parseTrunk() { |
| var loop = true; |
| var trunk = new StringBuffer(); |
| while (pattern.current != null && loop) { |
| loop = parseTrunkCharacter(trunk); |
| } |
| |
| if (zeroDigitCount == 0 && digitLeftCount > 0 && decimalPos >= 0) { |
| // Handle '###.###' and '###.' and '.###' |
| // Handle '.###' |
| var n = decimalPos == 0 ? 1 : decimalPos; |
| 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 FormatException('Malformed pattern "${pattern.input}"'); |
| } |
| var totalDigits = digitLeftCount + zeroDigitCount + digitRightCount; |
| |
| format.maximumFractionDigits = |
| decimalPos >= 0 ? totalDigits - decimalPos : 0; |
| if (decimalPos >= 0) { |
| format.minimumFractionDigits = |
| digitLeftCount + zeroDigitCount - decimalPos; |
| if (format.minimumFractionDigits < 0) { |
| format.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; |
| format.minimumIntegerDigits = effectiveDecimalPos - digitLeftCount; |
| if (format._useExponentialNotation) { |
| format.maximumIntegerDigits = |
| digitLeftCount + format.minimumIntegerDigits; |
| |
| // In exponential display, we need to at least show something. |
| if (format.maximumFractionDigits == 0 && |
| format.minimumIntegerDigits == 0) { |
| format.minimumIntegerDigits = 1; |
| } |
| } |
| |
| format._finalGroupingSize = max(0, groupingCount); |
| if (!format._groupingSizeSetExplicitly) { |
| format._groupingSize = format._finalGroupingSize; |
| } |
| format._decimalSeparatorAlwaysShown = |
| decimalPos == 0 || decimalPos == totalDigits; |
| |
| return trunk.toString(); |
| } |
| |
| /// Parse an individual character of the trunk. Return true if we should |
| /// continue to look for additional trunk characters or false if we have |
| /// reached the end. |
| bool parseTrunkCharacter(trunk) { |
| var ch = pattern.current; |
| switch (ch) { |
| case _PATTERN_DIGIT: |
| if (zeroDigitCount > 0) { |
| digitRightCount++; |
| } else { |
| digitLeftCount++; |
| } |
| if (groupingCount >= 0 && decimalPos < 0) { |
| groupingCount++; |
| } |
| break; |
| case _PATTERN_ZERO_DIGIT: |
| if (digitRightCount > 0) { |
| throw new FormatException( |
| 'Unexpected "0" in pattern "' + pattern.input + '"'); |
| } |
| zeroDigitCount++; |
| if (groupingCount >= 0 && decimalPos < 0) { |
| groupingCount++; |
| } |
| break; |
| case _PATTERN_GROUPING_SEPARATOR: |
| if (groupingCount > 0) { |
| format._groupingSizeSetExplicitly = true; |
| format._groupingSize = groupingCount; |
| } |
| groupingCount = 0; |
| break; |
| case _PATTERN_DECIMAL_SEPARATOR: |
| if (decimalPos >= 0) { |
| throw new FormatException( |
| 'Multiple decimal separators in pattern "$pattern"'); |
| } |
| decimalPos = digitLeftCount + zeroDigitCount + digitRightCount; |
| break; |
| case _PATTERN_EXPONENT: |
| trunk.write(ch); |
| if (format._useExponentialNotation) { |
| throw new FormatException( |
| 'Multiple exponential symbols in pattern "$pattern"'); |
| } |
| format._useExponentialNotation = true; |
| format.minimumExponentDigits = 0; |
| |
| // exponent pattern can have a optional '+'. |
| pattern.moveNext(); |
| var nextChar = pattern.current; |
| if (nextChar == _PATTERN_PLUS) { |
| trunk.write(pattern.current); |
| pattern.moveNext(); |
| format._useSignForPositiveExponent = true; |
| } |
| |
| // Use lookahead to parse out the exponential part |
| // of the pattern, then jump into phase 2. |
| while (pattern.current == _PATTERN_ZERO_DIGIT) { |
| trunk.write(pattern.current); |
| pattern.moveNext(); |
| format.minimumExponentDigits++; |
| } |
| |
| if ((digitLeftCount + zeroDigitCount) < 1 || |
| format.minimumExponentDigits < 1) { |
| throw new FormatException('Malformed exponential pattern "$pattern"'); |
| } |
| return false; |
| default: |
| return false; |
| } |
| trunk.write(ch); |
| pattern.moveNext(); |
| return true; |
| } |
| } |
| |
| /// Returns an [Iterable] on the string as a list of substrings. |
| Iterable _iterable(String s) => new _StringIterable(s); |
| |
| /// Return an iterator on the string as a list of substrings. |
| Iterator<String> _iterator(String s) => new _StringIterator(s); |
| |
| // TODO(nweiz): remove this when issue 3780 is fixed. |
| /// Provides an Iterable that wraps [_iterator] so it can be used in a `for` |
| /// loop. |
| class _StringIterable extends IterableBase<String> { |
| final Iterator<String> iterator; |
| |
| _StringIterable(String s) : iterator = _iterator(s); |
| } |
| |
| /// Provides an iterator over a string as a list of substrings, and also |
| /// gives us a lookahead of one via the [peek] method. |
| class _StringIterator implements Iterator<String> { |
| final String input; |
| int nextIndex = 0; |
| String _current = null; |
| |
| _StringIterator(input) : input = _validate(input); |
| |
| String get current => _current; |
| |
| bool moveNext() { |
| if (nextIndex >= input.length) { |
| _current = null; |
| return false; |
| } |
| _current = input[nextIndex++]; |
| return true; |
| } |
| |
| String get peek => nextIndex >= input.length ? null : input[nextIndex]; |
| |
| Iterator<String> get iterator => this; |
| |
| static String _validate(input) { |
| if (input is! String) throw new ArgumentError(input); |
| return input; |
| } |
| } |
| |
| /// Used primarily for currency formatting, this number-like class stores |
| /// millionths of a currency unit, typically as an Int64. |
| /// |
| /// It supports no operations other than being used for Intl number formatting. |
| abstract class MicroMoney { |
| factory MicroMoney(micros) => new _MicroMoney(micros); |
| } |
| |
| /// Used primarily for currency formatting, this stores millionths of a |
| /// currency unit, typically as an Int64. |
| /// |
| /// This private class provides the operations needed by the formatting code. |
| class _MicroMoney implements MicroMoney { |
| var _micros; |
| _MicroMoney(this._micros); |
| static const _multiplier = 1000000; |
| |
| get _integerPart => _micros ~/ _multiplier; |
| int get _fractionPart => (this - _integerPart)._micros.toInt().abs(); |
| |
| bool get isNegative => _micros.isNegative; |
| |
| _MicroMoney abs() => isNegative ? new _MicroMoney(_micros.abs()) : this; |
| |
| // Note that if this is done in a general way there's a risk of integer |
| // overflow on JS when multiplying out the [other] parameter, which may be |
| // an Int64. In formatting we only ever subtract out our own integer part. |
| _MicroMoney operator -(other) { |
| if (other is _MicroMoney) return new _MicroMoney(_micros - other._micros); |
| return new _MicroMoney(_micros - (other * _multiplier)); |
| } |
| |
| _MicroMoney operator +(other) { |
| if (other is _MicroMoney) return new _MicroMoney(_micros + other._micros); |
| return new _MicroMoney(_micros + (other * _multiplier)); |
| } |
| |
| _MicroMoney operator ~/(divisor) { |
| if (divisor is! int) { |
| throw new ArgumentError.value( |
| divisor, 'divisor', '_MicroMoney ~/ only supports int arguments.'); |
| } |
| return new _MicroMoney((_integerPart ~/ divisor) * _multiplier); |
| } |
| |
| _MicroMoney operator *(other) { |
| if (other is! int) { |
| throw new ArgumentError.value( |
| other, 'other', '_MicroMoney * only supports int arguments.'); |
| } |
| return new _MicroMoney( |
| (_integerPart * other) * _multiplier + (_fractionPart * other)); |
| } |
| |
| /// Note that this only really supports remainder from an int, |
| /// not division by another MicroMoney |
| _MicroMoney remainder(other) { |
| if (other is! int) { |
| throw new ArgumentError.value( |
| other, 'other', '_MicroMoney.remainder only supports int arguments.'); |
| } |
| return new _MicroMoney(_micros.remainder(other * _multiplier)); |
| } |
| |
| double toDouble() => _micros.toDouble() / _multiplier; |
| |
| int toInt() => _integerPart.toInt(); |
| |
| String toString() { |
| var beforeDecimal = _integerPart.toString(); |
| var decimalPart = ''; |
| var fractionPart = _fractionPart; |
| if (fractionPart != 0) { |
| decimalPart = '.' + fractionPart.toString(); |
| } |
| return '$beforeDecimal$decimalPart'; |
| } |
| } |