| // 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; |
| |
| // ignore_for_file: constant_identifier_names |
| |
| /// The function that we pass internally to NumberFormat to get |
| /// the appropriate pattern (e.g. currency) |
| typedef _PatternGetter = String Function(NumberSymbols); |
| |
| /// 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.percentPattern("ar"); var |
| /// eurosInUSFormat = new NumberFormat.currency(locale: "en_US", |
| /// symbol: "€"); |
| /// |
| /// There are several such constructors available, though some of them are |
| /// limited. For example, at the moment, scientificPattern prints only as |
| /// equivalent to "#E0" and does not take into account significant digits. |
| 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; |
| |
| static final _ln10 = log(10); |
| |
| /// 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 = StringBuffer(); |
| |
| /// Create a number format that prints using [newPattern] as it applies in |
| /// [locale]. |
| factory NumberFormat([String newPattern, String locale]) => |
| 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 PERCENT_PATTERN. |
| NumberFormat.decimalPercentPattern({String locale, int decimalDigits}) |
| : this._forPattern(locale, (x) => x.PERCENT_PATTERN, |
| decimalDigits: decimalDigits); |
| |
| /// 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 = RegExp(r'^[a-zA-Z]{3}$'); |
| |
| /// Create a number format that prints as CURRENCY_PATTERN. (Deprecated: |
| /// prefer NumberFormat.currency) |
| /// |
| /// If provided, |
| /// use [currencyNameOrSymbol] 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 NumberFormat.currency(locale: locale, name: currencyNameOrSymbol); |
| } else { |
| return 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. |
| /// |
| /// The [customPattern] parameter can be used to specify a particular |
| /// format. This is useful if you have your own locale data which includes |
| /// unsupported formats (e.g. accounting format for currencies.) |
| // TODO(alanknight): Should we allow decimalDigits on other numbers. |
| NumberFormat.currency( |
| {String locale, |
| String name, |
| String symbol, |
| int decimalDigits, |
| String customPattern}) |
| : this._forPattern(locale, (x) => customPattern ?? 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 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 final 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': 'Riyal', |
| '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 Function(NumberFormat) computeCurrencySymbol, |
| int decimalDigits, |
| bool isForCurrency = false}) |
| : _locale = Intl.verifiedLocale(locale, localeExists), |
| _isForCurrency = isForCurrency { |
| _currencySymbol = currencySymbol; |
| _decimalDigits = decimalDigits; |
| _symbols = numberFormatSymbols[_locale]; |
| _localeZero = _symbols.ZERO_DIGIT.codeUnitAt(0); |
| _zeroOffset = _localeZero - _zero; |
| _negativePrefix = _symbols.MINUS_SIGN; |
| currencyName = name ?? _symbols.DEF_CURRENCY_CODE; |
| if (_currencySymbol == null && computeCurrencySymbol != null) { |
| _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 _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 _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, int decimalDigits}) { |
| return _CompactNumberFormat( |
| locale: locale, |
| formatType: _CompactFormatType.COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN, |
| name: name, |
| getPattern: (symbols) => symbols.CURRENCY_PATTERN, |
| computeCurrencySymbol: (format) => |
| _simpleCurrencySymbols[format.currencyName] ?? format.currencyName, |
| decimalDigits: decimalDigits, |
| 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 _CompactNumberFormat( |
| locale: locale, |
| formatType: _CompactFormatType.COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN, |
| name: name, |
| getPattern: (symbols) => symbols.CURRENCY_PATTERN, |
| currencySymbol: symbol, |
| 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) => _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 integer limits. |
| // TODO(alanknight): Do we have a MaxInt constant we could use instead? |
| static final _maxInt = 1 is double ? pow(2, 52) : 1.0e300.floor(); |
| static final _maxDigits = (log(_maxInt) / log(10)).ceil(); |
| |
| /// Helpers to check numbers that don't conform to the [num] interface, |
| /// e.g. Int64 |
| bool _isInfinite(number) => number is num ? number.isInfinite : false; |
| bool _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. |
| dynamic _floor(dynamic number) { |
| if (number.isNegative && !number.abs().isNegative) { |
| throw 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. |
| dynamic _round(dynamic 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(dynamic number) { |
| dynamic integerPart; |
| int fractionPart; |
| int extraIntegerDigits; |
| var fractionDigits = maximumFractionDigits; |
| |
| var power = 0; |
| int 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 (fraction.toInt() != 0) { |
| // If the fractional part leftover is > 1, presumbly the number |
| // was too big for a fixed-size integer, so leave it as whatever |
| // it was - the obvious thing is a double. |
| integerPart = number; |
| fraction = 0; |
| } |
| |
| /// 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)) { |
| // Add the padding digits to the regular digits so that we get grouping. |
| var padding = '0' * (minimumIntegerDigits - digitLength); |
| integerDigits = '$padding$integerDigits'; |
| digitLength = integerDigits.length; |
| 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 integer part is larger than the maximum integer size |
| // (2^52 on Javascript, 2^63 on the VM) it will lose precision, |
| // so pad out the rest of it with zeros. |
| var paddingDigits = ''; |
| if (integerPart is num && integerPart > _maxInt) { |
| var howManyDigitsTooBig = (log(integerPart) / _ln10).ceil() - _maxDigits; |
| num divisor = pow(10, howManyDigitsTooBig).round(); |
| // pow() produces 0 if the result is too large for a 64-bit int. |
| // If that happens, use a floating point divisor instead. |
| if (divisor == 0) divisor = pow(10.0, howManyDigitsTooBig); |
| paddingDigits = '0' * 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 fractionLength = fractionPart.length; |
| while (fractionPart.codeUnitAt(fractionLength - 1) == _zero && |
| fractionLength > minimumFractionDigits + 1) { |
| fractionLength--; |
| } |
| for (var i = 1; i < fractionLength; i++) { |
| _addDigit(fractionPart.codeUnitAt(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(x + _zeroOffset); |
| } |
| |
| void _pad(int numberOfDigits, String basic) { |
| if (_zeroOffset == 0) { |
| _buffer.write(basic.padLeft(numberOfDigits, '0')); |
| } else { |
| _slowPad(numberOfDigits, basic); |
| } |
| } |
| |
| /// Print padding up to [numberOfDigits] above what's included in [basic]. |
| void _slowPad(int numberOfDigits, String basic) { |
| for (var i = 0; i < numberOfDigits - basic.length; i++) { |
| _add(symbols.ZERO_DIGIT); |
| } |
| for (var i = 0; i < basic.length; i++) { |
| _addDigit(basic.codeUnitAt(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); |
| } |
| } |
| |
| /// The code point for the character '0'. |
| static const _zero = 48; |
| |
| /// The code point for the locale's zero digit. |
| /// |
| /// Initialized when the locale is set. |
| int _localeZero = 0; |
| |
| /// The difference between our zero and '0'. |
| /// |
| /// In other words, a constant _localeZero - _zero. Initialized when |
| /// the locale is set. |
| int _zeroOffset = 0; |
| |
| /// 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 = |
| _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 = 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 => NumberFormat._zero; |
| int get _localeZero => format._localeZero; |
| |
| /// Create a new [_NumberParser] on which we can call parse(). |
| _NumberParser(this.format, this.text) : input = _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, |
| '+': () => '+', |
| '-': () => '-', |
| }; |
| |
| void invalidFormat() => |
| throw 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' |
| void 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) => |
| prefix.isNotEmpty && input.startsWith(prefix); |
| |
| // 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)) gotPositive = true; |
| if (checkPrefix(_negativePrefix)) gotNegative = true; |
| |
| // The positive prefix might be a substring of the negative, in |
| // which case both would match. |
| if (gotPositive && gotNegative) { |
| if (_positivePrefix.length > _negativePrefix.length) { |
| gotNegative = false; |
| } else if (_negativePrefix.length > _positivePrefix.length) { |
| gotPositive = false; |
| } |
| } |
| if (skip) { |
| if (gotPositive) input.read(_positivePrefix.length); |
| if (gotNegative) input.read(_negativePrefix.length); |
| } |
| } |
| |
| /// 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 0.0 / 0.0; |
| if (text == '$_positivePrefix${symbols.INFINITY}$_positiveSuffix') { |
| return 1.0 / 0.0; |
| } |
| if (text == '$_negativePrefix${symbols.INFINITY}$_negativeSuffix') { |
| return -1.0 / 0.0; |
| } |
| |
| 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 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()) { |
| var digit = asDigit(input.peek()); |
| if (digit != null) { |
| _normalized.writeCharCode(_zero + digit); |
| input.next(); |
| } else { |
| processNonDigit(); |
| } |
| checkSuffixes(); |
| } |
| |
| var normalizedText = _normalized.toString(); |
| num parsed = int.tryParse(normalizedText); |
| 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 FormatException( |
| 'Positive and negative trunks must be the same', trunk); |
| } |
| 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 = 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 FormatException('Too many percent/permill', format); |
| } |
| format._multiplier = _PERCENT_SCALE; |
| affix.write(symbols.PERCENT); |
| break; |
| case _PATTERN_PER_MILLE: |
| if (format._multiplier != 1 && |
| format._multiplier != _PER_MILLE_SCALE) { |
| throw FormatException('Too many percent/permill', format); |
| } |
| 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 = 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 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 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 FormatException( |
| 'Multiple decimal separators in pattern "$pattern"'); |
| } |
| decimalPos = digitLeftCount + zeroDigitCount + digitRightCount; |
| break; |
| case _PATTERN_EXPONENT: |
| trunk.write(ch); |
| if (format._useExponentialNotation) { |
| throw 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 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<String> _iterable(String s) => _StringIterable(s); |
| |
| /// Return an iterator on the string as a list of substrings. |
| Iterator<String> _iterator(String s) => _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; |
| |
| _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 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) => _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 { |
| final dynamic _micros; |
| _MicroMoney(this._micros); |
| static const _multiplier = 1000000; |
| |
| dynamic get _integerPart => _micros ~/ _multiplier; |
| int get _fractionPart => (this - _integerPart)._micros.toInt().abs(); |
| |
| bool get isNegative => _micros.isNegative; |
| |
| _MicroMoney abs() => isNegative ? _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 _MicroMoney(_micros - other._micros); |
| return _MicroMoney(_micros - (other * _multiplier)); |
| } |
| |
| _MicroMoney operator +(other) { |
| if (other is _MicroMoney) return _MicroMoney(_micros + other._micros); |
| return _MicroMoney(_micros + (other * _multiplier)); |
| } |
| |
| _MicroMoney operator ~/(divisor) { |
| if (divisor is! int) { |
| throw ArgumentError.value( |
| divisor, 'divisor', '_MicroMoney ~/ only supports int arguments.'); |
| } |
| return _MicroMoney((_integerPart ~/ divisor) * _multiplier); |
| } |
| |
| _MicroMoney operator *(other) { |
| if (other is! int) { |
| throw ArgumentError.value( |
| other, 'other', '_MicroMoney * only supports int arguments.'); |
| } |
| return _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 ArgumentError.value( |
| other, 'other', '_MicroMoney.remainder only supports int arguments.'); |
| } |
| return _MicroMoney(_micros.remainder(other * _multiplier)); |
| } |
| |
| double toDouble() => _micros.toDouble() / _multiplier; |
| |
| int toInt() => _integerPart.toInt(); |
| |
| String toString() { |
| var beforeDecimal = '$_integerPart'; |
| var decimalPart = ''; |
| var fractionPart = _fractionPart; |
| if (fractionPart != 0) { |
| decimalPart = '.$fractionPart'; |
| } |
| return '$beforeDecimal$decimalPart'; |
| } |
| } |