Split up `number_format.dart`.
PiperOrigin-RevId: 328335245
diff --git a/lib/intl.dart b/lib/intl.dart
index 880dbcc..7b0c6b9 100644
--- a/lib/intl.dart
+++ b/lib/intl.dart
@@ -29,7 +29,8 @@
export 'src/intl/bidi.dart' show Bidi;
export 'src/intl/bidi_formatter.dart' show BidiFormatter;
export 'src/intl/date_format.dart' show DateFormat;
-export 'src/intl/number_format.dart' show NumberFormat, MicroMoney;
+export 'src/intl/micro_money.dart' show MicroMoney;
+export 'src/intl/number_format.dart' show NumberFormat;
export 'src/intl/text_direction.dart' show TextDirection;
/// The Intl class provides a common entry point for internationalization
diff --git a/lib/src/intl/compact_number_format.dart b/lib/src/intl/compact_number_format.dart
index fd53814..99f5280 100644
--- a/lib/src/intl/compact_number_format.dart
+++ b/lib/src/intl/compact_number_format.dart
@@ -192,10 +192,10 @@
locale = Intl.verifiedLocale(locale, NumberFormat.localeExists);
var symbols = numberFormatSymbols[locale];
var localeZero = symbols.ZERO_DIGIT.codeUnitAt(0);
- var zeroOffset = localeZero - NumberFormat._zero;
+ var zeroOffset = localeZero - constants.asciiZeroCodeUnit;
name ??= symbols.DEF_CURRENCY_CODE;
if (currencySymbol == null && lookupSimpleCurrencySymbol) {
- currencySymbol = NumberFormat._simpleCurrencySymbols[name];
+ currencySymbol = constants.simpleCurrencySymbols[name];
}
currencySymbol ??= name;
var pattern = getPattern(symbols);
@@ -256,7 +256,7 @@
pattern,
symbols,
zeroOffset,
- _NumberFormatParser.parse(symbols, pattern, isForCurrency,
+ NumberFormatParser.parse(symbols, pattern, isForCurrency,
currencySymbol, name, decimalDigits),
styles);
}
@@ -270,7 +270,7 @@
String pattern,
NumberSymbols symbols,
int zeroOffset,
- _NumberFormatParseResult result,
+ NumberFormatParseResult result,
// Fields introduced in this class.
this._styles)
: super._(currencyName, currencySymbol, isForCurrency, locale, localeZero,
diff --git a/lib/src/intl/constants.dart b/lib/src/intl/constants.dart
index 51ac779..83e2607 100644
--- a/lib/src/intl/constants.dart
+++ b/lib/src/intl/constants.dart
@@ -3,3 +3,166 @@
// BSD-style license that can be found in the LICENSE file.
final int asciiZeroCodeUnit = '0'.codeUnitAt(0);
+
+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',
+};
diff --git a/lib/src/intl/micro_money.dart b/lib/src/intl/micro_money.dart
new file mode 100644
index 0000000..f773660
--- /dev/null
+++ b/lib/src/intl/micro_money.dart
@@ -0,0 +1,83 @@
+// Copyright (c) 2020, 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
+
+/// 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';
+ }
+}
diff --git a/lib/src/intl/number_format.dart b/lib/src/intl/number_format.dart
index be92ab8..d3d8454 100644
--- a/lib/src/intl/number_format.dart
+++ b/lib/src/intl/number_format.dart
@@ -3,14 +3,15 @@
// 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';
+import 'constants.dart' as constants;
+import 'number_format_parser.dart';
+import 'number_parser.dart';
part 'compact_number_format.dart';
@@ -60,10 +61,10 @@
/// equivalent to "#E0" and does not take into account significant digits.
class NumberFormat {
/// Variables to determine how number printing behaves.
- final String _negativePrefix;
- final String _positivePrefix;
- final String _negativeSuffix;
- final String _positiveSuffix;
+ final String negativePrefix;
+ final String positivePrefix;
+ final String negativeSuffix;
+ final 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
@@ -106,9 +107,9 @@
/// For percent and permille, what are we multiplying by in order to
/// get the printed value, e.g. 100 for percent.
- final int _multiplier;
+ final int multiplier;
- /// How many digits are there in the [_multiplier].
+ /// How many digits are there in the [multiplier].
final int _multiplierDigits;
/// Stores the pattern used to create this format. This isn't used, but
@@ -295,182 +296,7 @@
/// (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',
- };
+ constants.simpleCurrencySymbols[currencyCode] ?? currencyCode;
/// Create a number format that prints in a pattern we get from
/// the [getPattern] function using the locale [locale].
@@ -487,10 +313,10 @@
locale = Intl.verifiedLocale(locale, localeExists);
var symbols = numberFormatSymbols[locale];
var localeZero = symbols.ZERO_DIGIT.codeUnitAt(0);
- var zeroOffset = localeZero - _zero;
+ var zeroOffset = localeZero - constants.asciiZeroCodeUnit;
name ??= symbols.DEF_CURRENCY_CODE;
if (currencySymbol == null && lookupSimpleCurrencySymbol) {
- currencySymbol = _simpleCurrencySymbols[name];
+ currencySymbol = constants.simpleCurrencySymbols[name];
}
currencySymbol ??= name;
@@ -505,7 +331,7 @@
pattern,
symbols,
zeroOffset,
- _NumberFormatParser.parse(symbols, pattern, isForCurrency,
+ NumberFormatParser.parse(symbols, pattern, isForCurrency,
currencySymbol, name, decimalDigits));
}
@@ -514,16 +340,16 @@
this.currencySymbol,
this._isForCurrency,
this._locale,
- this._localeZero,
+ this.localeZero,
this._pattern,
this._symbols,
this._zeroOffset,
- _NumberFormatParseResult result)
- : _positivePrefix = result.positivePrefix,
- _negativePrefix = result.negativePrefix,
- _positiveSuffix = result.positiveSuffix,
- _negativeSuffix = result.negativeSuffix,
- _multiplier = result.multiplier,
+ NumberFormatParseResult result)
+ : positivePrefix = result.positivePrefix,
+ negativePrefix = result.negativePrefix,
+ positiveSuffix = result.positiveSuffix,
+ negativeSuffix = result.negativeSuffix,
+ multiplier = result.multiplier,
_multiplierDigits = result.multiplierDigits,
_useExponentialNotation = result.useExponentialNotation,
minimumExponentDigits = result.minimumExponentDigits,
@@ -613,7 +439,7 @@
/// Parse the number represented by the string. If it's not
/// parseable, throws a [FormatException].
- num parse(String text) => _NumberParser(this, text).value;
+ 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) {
@@ -789,7 +615,7 @@
}
}
power = pow(10, fractionDigits);
- digitMultiplier = power * _multiplier;
+ 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
@@ -871,7 +697,8 @@
/// 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 &&
+ while (fractionPart.codeUnitAt(fractionLength - 1) ==
+ constants.asciiZeroCodeUnit &&
fractionLength > minimumFractionDigits + 1) {
fractionLength--;
}
@@ -943,13 +770,10 @@
}
}
- /// 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.
- final int _localeZero;
+ final int localeZero;
/// The difference between our zero and '0'.
///
@@ -959,11 +783,11 @@
/// 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;
+ 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;
+ String _signSuffix(x) => x.isNegative ? negativeSuffix : positiveSuffix;
/// Explicitly turn off any grouping (e.g. by thousands) in this format.
///
@@ -977,715 +801,4 @@
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._multiplier;
- 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;
- }
-}
-
-/// Output of [_NumberFormatParser.parse].
-///
-/// Everything needed to initialize a [NumberFormat].
-class _NumberFormatParseResult {
- String negativePrefix;
- String positivePrefix = '';
- String negativeSuffix = '';
- String positiveSuffix = '';
-
- int multiplier = 1;
- int get multiplierDigits => (log(multiplier) / _ln10).round();
-
- int minimumExponentDigits = 0;
-
- int maximumIntegerDigits = 40;
- int minimumIntegerDigits = 1;
- int maximumFractionDigits = 3;
- int minimumFractionDigits = 0;
-
- int groupingSize = 3;
- int finalGroupingSize = 3;
-
- bool decimalSeparatorAlwaysShown = false;
- bool useSignForPositiveExponent = false;
- bool useExponentialNotation = false;
-
- int decimalDigits;
-
- // [decimalDigits] is both input and output of parsing.
- _NumberFormatParseResult(NumberSymbols symbols, this.decimalDigits) {
- negativePrefix = symbols.MINUS_SIGN;
- }
-}
-
-/// 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 NumberSymbols symbols;
-
- /// The pattern we are parsing.
- final _StringIterator pattern;
-
- /// Whether this is a currency.
- final bool isForCurrency;
-
- /// We can be passed a specific currency symbol, regardless of the locale.
- final String currencySymbol;
-
- final String currencyName;
-
- // The result being constructed.
- final _NumberFormatParseResult result;
-
- bool groupingSizeSetExplicitly = false;
-
- /// Create a new [_NumberFormatParser] for a particular [NumberFormat] and
- /// [input] pattern.
- ///
- /// [decimalDigits] is optional, if specified it overrides the default.
- _NumberFormatParser(this.symbols, String input, this.isForCurrency,
- this.currencySymbol, this.currencyName, int decimalDigits)
- : result = _NumberFormatParseResult(symbols, decimalDigits),
- pattern = _iterator(input) {
- pattern.moveNext();
- }
-
- static _NumberFormatParseResult parse(
- NumberSymbols symbols,
- String input,
- bool isForCurrency,
- String currencySymbol,
- String currencyName,
- int decimalDigits) =>
- input == null
- ? _NumberFormatParseResult(symbols, decimalDigits)
- : (_NumberFormatParser(symbols, input, isForCurrency, currencySymbol,
- currencyName, decimalDigits)
- .._parse())
- .result;
-
- /// 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'];
-
- /// Parse the input pattern and update [result].
- void _parse() {
- result.positivePrefix = _parseAffix();
- var trunk = _parseTrunk();
- result.positiveSuffix = _parseAffix();
- // If we have separate positive and negative patterns, now parse the
- // the negative version.
- if (pattern.current == _NumberFormatParser._PATTERN_SEPARATOR) {
- pattern.moveNext();
- result.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();
- }
- result.negativeSuffix = _parseAffix();
- } else {
- // If no negative affix is specified, they share the same positive affix.
- result.negativePrefix = result.negativePrefix + result.positivePrefix;
- result.negativeSuffix = result.positiveSuffix + result.negativeSuffix;
- }
-
- if (isForCurrency) {
- result.decimalDigits ??= _defaultDecimalDigits;
- }
- if (result.decimalDigits != null) {
- result.minimumFractionDigits = result.decimalDigits;
- result.maximumFractionDigits = result.decimalDigits;
- }
- }
-
- /// 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 (result.multiplier != 1 && result.multiplier != _PERCENT_SCALE) {
- throw const FormatException('Too many percent/permill');
- }
- result.multiplier = _PERCENT_SCALE;
- affix.write(symbols.PERCENT);
- break;
- case _PATTERN_PER_MILLE:
- if (result.multiplier != 1 && result.multiplier != _PER_MILLE_SCALE) {
- throw const FormatException('Too many percent/permill');
- }
- result.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;
-
- result.maximumFractionDigits =
- decimalPos >= 0 ? totalDigits - decimalPos : 0;
- if (decimalPos >= 0) {
- result.minimumFractionDigits =
- digitLeftCount + zeroDigitCount - decimalPos;
- if (result.minimumFractionDigits < 0) {
- result.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;
- result.minimumIntegerDigits = effectiveDecimalPos - digitLeftCount;
- if (result.useExponentialNotation) {
- result.maximumIntegerDigits =
- digitLeftCount + result.minimumIntegerDigits;
-
- // In exponential display, we need to at least show something.
- if (result.maximumFractionDigits == 0 &&
- result.minimumIntegerDigits == 0) {
- result.minimumIntegerDigits = 1;
- }
- }
-
- result.finalGroupingSize = max(0, groupingCount);
- if (!groupingSizeSetExplicitly) {
- result.groupingSize = result.finalGroupingSize;
- }
- result.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) {
- groupingSizeSetExplicitly = true;
- result.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 (result.useExponentialNotation) {
- throw FormatException(
- 'Multiple exponential symbols in pattern "$pattern"');
- }
- result.useExponentialNotation = true;
- result.minimumExponentDigits = 0;
-
- // exponent pattern can have a optional '+'.
- pattern.moveNext();
- var nextChar = pattern.current;
- if (nextChar == _PATTERN_PLUS) {
- trunk.write(pattern.current);
- pattern.moveNext();
- result.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();
- result.minimumExponentDigits++;
- }
-
- if ((digitLeftCount + zeroDigitCount) < 1 ||
- result.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';
- }
-}
-
final _ln10 = log(10);
diff --git a/lib/src/intl/number_format_parser.dart b/lib/src/intl/number_format_parser.dart
new file mode 100644
index 0000000..a2609b1
--- /dev/null
+++ b/lib/src/intl/number_format_parser.dart
@@ -0,0 +1,369 @@
+// Copyright (c) 2020, 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:math';
+
+import 'package:intl/number_symbols.dart';
+import 'package:intl/number_symbols_data.dart';
+
+import 'string_iterator.dart';
+
+// ignore_for_file: constant_identifier_names
+
+/// Output of [_NumberFormatParser.parse].
+///
+/// Everything needed to initialize a [NumberFormat].
+class NumberFormatParseResult {
+ String negativePrefix;
+ String positivePrefix = '';
+ String negativeSuffix = '';
+ String positiveSuffix = '';
+
+ int multiplier = 1;
+ int get multiplierDigits => (log(multiplier) / _ln10).round();
+
+ int minimumExponentDigits = 0;
+
+ int maximumIntegerDigits = 40;
+ int minimumIntegerDigits = 1;
+ int maximumFractionDigits = 3;
+ int minimumFractionDigits = 0;
+
+ int groupingSize = 3;
+ int finalGroupingSize = 3;
+
+ bool decimalSeparatorAlwaysShown = false;
+ bool useSignForPositiveExponent = false;
+ bool useExponentialNotation = false;
+
+ int decimalDigits;
+
+ // [decimalDigits] is both input and output of parsing.
+ NumberFormatParseResult(NumberSymbols symbols, this.decimalDigits) {
+ negativePrefix = symbols.MINUS_SIGN;
+ }
+}
+
+/// 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 NumberSymbols symbols;
+
+ /// The pattern we are parsing.
+ final StringIterator pattern;
+
+ /// Whether this is a currency.
+ final bool isForCurrency;
+
+ /// We can be passed a specific currency symbol, regardless of the locale.
+ final String currencySymbol;
+
+ final String currencyName;
+
+ // The result being constructed.
+ final NumberFormatParseResult result;
+
+ bool groupingSizeSetExplicitly = false;
+
+ /// Create a new [_NumberFormatParser] for a particular [NumberFormat] and
+ /// [input] pattern.
+ ///
+ /// [decimalDigits] is optional, if specified it overrides the default.
+ NumberFormatParser(this.symbols, String input, this.isForCurrency,
+ this.currencySymbol, this.currencyName, int decimalDigits)
+ : result = NumberFormatParseResult(symbols, decimalDigits),
+ pattern = StringIterator(input) {
+ pattern.moveNext();
+ }
+
+ static NumberFormatParseResult parse(
+ NumberSymbols symbols,
+ String input,
+ bool isForCurrency,
+ String currencySymbol,
+ String currencyName,
+ int decimalDigits) =>
+ input == null
+ ? NumberFormatParseResult(symbols, decimalDigits)
+ : (NumberFormatParser(symbols, input, isForCurrency, currencySymbol,
+ currencyName, decimalDigits)
+ .._parse())
+ .result;
+
+ /// 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'];
+
+ /// Parse the input pattern and update [result].
+ void _parse() {
+ result.positivePrefix = _parseAffix();
+ var trunk = _parseTrunk();
+ result.positiveSuffix = _parseAffix();
+ // If we have separate positive and negative patterns, now parse the
+ // the negative version.
+ if (pattern.current == NumberFormatParser.PATTERN_SEPARATOR) {
+ pattern.moveNext();
+ result.negativePrefix = _parseAffix();
+ // Skip over the negative trunk, verifying that it's identical to the
+ // positive trunk.
+ for (var each in StringIterable(trunk)) {
+ if (pattern.current != each && pattern.current != null) {
+ throw FormatException(
+ 'Positive and negative trunks must be the same', trunk);
+ }
+ pattern.moveNext();
+ }
+ result.negativeSuffix = _parseAffix();
+ } else {
+ // If no negative affix is specified, they share the same positive affix.
+ result.negativePrefix = result.negativePrefix + result.positivePrefix;
+ result.negativeSuffix = result.positiveSuffix + result.negativeSuffix;
+ }
+
+ if (isForCurrency) {
+ result.decimalDigits ??= _defaultDecimalDigits;
+ }
+ if (result.decimalDigits != null) {
+ result.minimumFractionDigits = result.decimalDigits;
+ result.maximumFractionDigits = result.decimalDigits;
+ }
+ }
+
+ /// 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 (result.multiplier != 1 && result.multiplier != PERCENT_SCALE) {
+ throw const FormatException('Too many percent/permill');
+ }
+ result.multiplier = PERCENT_SCALE;
+ affix.write(symbols.PERCENT);
+ break;
+ case PATTERN_PER_MILLE:
+ if (result.multiplier != 1 && result.multiplier != PER_MILLE_SCALE) {
+ throw const FormatException('Too many percent/permill');
+ }
+ result.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;
+
+ result.maximumFractionDigits =
+ decimalPos >= 0 ? totalDigits - decimalPos : 0;
+ if (decimalPos >= 0) {
+ result.minimumFractionDigits =
+ digitLeftCount + zeroDigitCount - decimalPos;
+ if (result.minimumFractionDigits < 0) {
+ result.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;
+ result.minimumIntegerDigits = effectiveDecimalPos - digitLeftCount;
+ if (result.useExponentialNotation) {
+ result.maximumIntegerDigits =
+ digitLeftCount + result.minimumIntegerDigits;
+
+ // In exponential display, we need to at least show something.
+ if (result.maximumFractionDigits == 0 &&
+ result.minimumIntegerDigits == 0) {
+ result.minimumIntegerDigits = 1;
+ }
+ }
+
+ result.finalGroupingSize = max(0, groupingCount);
+ if (!groupingSizeSetExplicitly) {
+ result.groupingSize = result.finalGroupingSize;
+ }
+ result.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) {
+ groupingSizeSetExplicitly = true;
+ result.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 (result.useExponentialNotation) {
+ throw FormatException(
+ 'Multiple exponential symbols in pattern "$pattern"');
+ }
+ result.useExponentialNotation = true;
+ result.minimumExponentDigits = 0;
+
+ // exponent pattern can have a optional '+'.
+ pattern.moveNext();
+ var nextChar = pattern.current;
+ if (nextChar == PATTERN_PLUS) {
+ trunk.write(pattern.current);
+ pattern.moveNext();
+ result.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();
+ result.minimumExponentDigits++;
+ }
+
+ if ((digitLeftCount + zeroDigitCount) < 1 ||
+ result.minimumExponentDigits < 1) {
+ throw FormatException('Malformed exponential pattern "$pattern"');
+ }
+ return false;
+ default:
+ return false;
+ }
+ trunk.write(ch);
+ pattern.moveNext();
+ return true;
+ }
+}
+
+final _ln10 = log(10);
diff --git a/lib/src/intl/number_parser.dart b/lib/src/intl/number_parser.dart
new file mode 100644
index 0000000..318a18b
--- /dev/null
+++ b/lib/src/intl/number_parser.dart
@@ -0,0 +1,242 @@
+// Copyright (c) 2020, 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 'package:intl/number_symbols.dart';
+
+import 'constants.dart' as constants;
+import 'intl_stream.dart';
+import 'number_format.dart';
+import 'number_format_parser.dart';
+
+/// 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 _localeZero => format.localeZero;
+
+ /// Create a new [_NumberParser] on which we can call parse().
+ NumberParser(this.format, this.text) : input = IntlStream(text) {
+ scale = format.multiplier;
+ 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(constants.asciiZeroCodeUnit + digit);
+ input.next();
+ } else {
+ processNonDigit();
+ }
+ checkSuffixes();
+ }
+
+ var normalizedText = _normalized.toString();
+ num parsed = int.tryParse(normalizedText);
+ parsed ??= double.parse(normalizedText);
+ return parsed / scale;
+ }
+}
diff --git a/lib/src/intl/string_iterator.dart b/lib/src/intl/string_iterator.dart
new file mode 100644
index 0000000..839574d
--- /dev/null
+++ b/lib/src/intl/string_iterator.dart
@@ -0,0 +1,45 @@
+// Copyright (c) 2020, 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
+
+// TODO(nweiz): remove this when issue 3780 is fixed.
+import 'dart:collection';
+
+/// 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 = StringIterator(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;
+ }
+}