blob: 487e2e35ab520bc26835eac69efd210e55e9330c [file] [log] [blame]
// 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.
// @dart=2.9
import 'dart:collection';
import 'dart:math';
import 'package:intl/intl.dart';
import 'package:intl/number_symbols.dart';
import 'package:intl/number_symbols_data.dart';
import 'intl_stream.dart';
part 'compact_number_format.dart';
// 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 = 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 = NumberFormat.percentPattern("ar"); var
/// eurosInUSFormat = 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,
/// NumberFormat.currency(name: 'USD', decimalDigits: 7)
/// will format with 7 decimal digits, because that's what we asked for. But
/// 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.
/// 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 = 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 = 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 = 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.
/// NumberFormat.currency(name: 'USD', decimalDigits: 7)
/// will format with 7 decimal digits, because that's what we asked for. But
/// 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.
/// 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.
/// NumberFormat.simpleCurrency(name: 'USD', decimalDigits: 7)
/// will format with 7 decimal digits, because that's what we asked for. But
/// 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.
/// 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 IntlStream 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 = IntlStream(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(IntlStream 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';
}
}