Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4)

Unified Diff: packages/intl/lib/src/intl/number_format.dart

Issue 2989763002: Update charted to 0.4.8 and roll (Closed)
Patch Set: Removed Cutch from list of reviewers Created 3 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « packages/intl/lib/src/intl/date_format_helpers.dart ('k') | packages/intl/lib/src/intl_helpers.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: packages/intl/lib/src/intl/number_format.dart
diff --git a/packages/intl/lib/src/intl/number_format.dart b/packages/intl/lib/src/intl/number_format.dart
index 9e0de51c8846f5971dafa85da0b935e6ad7575a5..ae6428b304a27fb31730be7c0c36cf48785ed9ca 100644
--- a/packages/intl/lib/src/intl/number_format.dart
+++ b/packages/intl/lib/src/intl/number_format.dart
@@ -4,177 +4,560 @@
part of intl;
-/**
- * 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, indicates 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 are present
- *
- * For example,
- * var f = new NumberFormat("###.0#", "en_US");
- * print(f.format(12.345));
- * ==> 12.34
- * If the locale is not specified, it will default to the current locale. If
- * the format is not specified it will print in a basic format with at least
- * one integer digit and three fraction digits.
- *
- * There are also standard patterns available via the special constructors. e.g.
- * var percent = new NumberFormat.percentFormat("ar");
- * var eurosInUSFormat = new NumberFormat.currencyPattern("en_US", "€");
- * There are four such constructors: decimalFormat, percentFormat,
- * scientificFormat and currencyFormat. However, at the moment,
- * scientificFormat prints only as equivalent to "#E0" and does not take
- * into account significant digits. The currencyFormat will default to the
- * three-letter name of the currency if no explicit name/symbol is provided.
- */
+/// The function that we pass internally to NumberFormat to get
+/// the appropriate pattern (e.g. currency)
+typedef String _PatternGetter(NumberSymbols symbols);
+
+/// Provides the ability to format a number in a locale-specific way. The
+/// format is specified as a pattern using a subset of the ICU formatting
+/// patterns.
+///
+/// - `0` A single digit
+/// - `#` A single digit, omitted if the value is zero
+/// - `.` Decimal separator
+/// - `-` Minus sign
+/// - `,` Grouping separator
+/// - `E` Separates mantissa and expontent
+/// - `+` - Before an exponent, to say it should be prefixed with a plus sign.
+/// - `%` - In prefix or suffix, multiply by 100 and show as percentage
+/// - `‰ (\u2030)` In prefix or suffix, multiply by 1000 and show as per mille
+/// - `¤ (\u00A4)` Currency sign, replaced by currency name
+/// - `'` Used to quote special characters
+/// - `;` Used to separate the positive and negative patterns (if both present)
+///
+/// For example,
+/// var f = new NumberFormat("###.0#", "en_US");
+/// print(f.format(12.345));
+/// ==> 12.34
+/// If the locale is not specified, it will default to the current locale. If
+/// the format is not specified it will print in a basic format with at least
+/// one integer digit and three fraction digits.
+///
+/// There are also standard patterns available via the special constructors.
+/// e.g.
+/// var percent = new NumberFormat.percentFormat("ar");
+/// var eurosInUSFormat = new NumberFormat.currency(locale: "en_US",
+/// symbol: "€");
+/// There are four such constructors: decimalFormat, percentFormat,
+/// scientificFormat and currencyFormat. However, at the moment,
+/// scientificFormat prints only as equivalent to "#E0" and does not take
+/// into account significant digits. The currencyFormat will default to the
+/// three-letter name of the currency if no explicit name/symbol is provided.
class NumberFormat {
- /** Variables to determine how number printing behaves. */
+ /// 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.
- */
+
+ /// 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.
- */
+
+ /// 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.
- */
+
+ /// Set to true if the format has explicitly set the grouping size.
bool _groupingSizeSetExplicitly = false;
bool _decimalSeparatorAlwaysShown = false;
bool _useSignForPositiveExponent = false;
bool _useExponentialNotation = false;
+ /// Explicitly store if we are a currency format, and so should use the
+ /// appropriate number of decimal digits for a currency.
+ // TODO(alanknight): Handle currency formats which are specified in a raw
+ /// pattern, not using one of the currency constructors.
+ bool _isForCurrency = false;
+
int maximumIntegerDigits = 40;
int minimumIntegerDigits = 1;
int maximumFractionDigits = 3;
int minimumFractionDigits = 0;
int minimumExponentDigits = 0;
+ int _significantDigits = 0;
+
+ /// How many significant digits should we print.
+ ///
+ /// Note that if significantDigitsInUse is the default false, this
+ /// will be ignored.
+ int get significantDigits => _significantDigits;
+ set significantDigits(int x) {
+ _significantDigits = x;
+ significantDigitsInUse = true;
+ }
+
+ bool significantDigitsInUse = false;
- /**
- * For percent and permille, what are we multiplying by in order to
- * get the printed value, e.g. 100 for percent.
- */
+ /// 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]. */
+ /// 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.
- */
+ /// 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. */
+ /// The locale in which we print numbers.
final String _locale;
- /** Caches the symbols used for our locale. */
+ /// Caches the symbols used for our locale.
NumberSymbols _symbols;
- /** The name (or symbol) of the currency to print. */
+ /// The name of the currency to print, in ISO 4217 form.
String currencyName;
- /**
- * 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.
- */
+ /// The symbol to be used when formatting this as currency.
+ ///
+ /// For example, "$", "US$", or "€".
+ String _currencySymbol;
+
+ /// The symbol to be used when formatting this as currency.
+ ///
+ /// For example, "$", "US$", or "€".
+ String get currencySymbol => _currencySymbol ?? currencyName;
+
+ /// The number of decimal places to use when formatting.
+ ///
+ /// If this is not explicitly specified in the constructor, then for
+ /// currencies we use the default value for the currency if the name is given,
+ /// otherwise we use the value from the pattern for the locale.
+ ///
+ /// So, for example,
+ /// new NumberFormat.currency(name: 'USD', decimalDigits: 7)
+ /// will format with 7 decimal digits, because that's what we asked for. But
+ /// new NumberFormat.currency(locale: 'en_US', name: 'JPY')
+ /// will format with zero, because that's the default for JPY, and the
+ /// currency's default takes priority over the locale's default.
+ /// new NumberFormat.currency(locale: 'en_US')
+ /// will format with two, which is the default for that locale.
+ ///
+ int get decimalDigits => _decimalDigits;
+
+ int _decimalDigits;
+
+ /// For currencies, the default number of decimal places to use in
+ /// formatting. Defaults to two for non-currencies or currencies where it's
+ /// not specified.
+ int get _defaultDecimalDigits =>
+ currencyFractionDigits[currencyName.toUpperCase()] ??
+ currencyFractionDigits['DEFAULT'];
+
+ /// If we have a currencyName, use the decimal digits for that currency,
+ /// unless we've explicitly specified some other number.
+ bool get _overridesDecimalDigits => decimalDigits != null || _isForCurrency;
+
+ /// Transient internal state in which to build up the result of the format
+ /// operation. We can have this be just an instance variable because Dart is
+ /// single-threaded and unless we do an asynchronous operation in the process
+ /// of formatting then there will only ever be one number being formatted
+ /// at a time. In languages with threads we'd need to pass this on the stack.
final StringBuffer _buffer = new StringBuffer();
- /**
- * Create a number format that prints using [newPattern] as it applies in
- * [locale].
- */
+ /// Create a number format that prints using [newPattern] as it applies in
+ /// [locale].
factory NumberFormat([String newPattern, String locale]) =>
new NumberFormat._forPattern(locale, (x) => newPattern);
- /** Create a number format that prints as DECIMAL_PATTERN. */
+ /// 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. */
+ /// Create a number format that prints as PERCENT_PATTERN.
NumberFormat.percentPattern([String locale])
: this._forPattern(locale, (x) => x.PERCENT_PATTERN);
- /** Create a number format that prints as SCIENTIFIC_PATTERN. */
+ /// Create a number format that prints as SCIENTIFIC_PATTERN.
NumberFormat.scientificPattern([String locale])
: this._forPattern(locale, (x) => x.SCIENTIFIC_PATTERN);
- /**
- * Create a number format that prints as CURRENCY_PATTERN. If provided,
- * use [nameOrSymbol] in place of the default currency name. e.g.
- * var eurosInCurrentLocale = new NumberFormat
- * .currencyPattern(Intl.defaultLocale, "€");
- */
- NumberFormat.currencyPattern([String locale, String nameOrSymbol])
- : this._forPattern(locale, (x) => x.CURRENCY_PATTERN, nameOrSymbol);
-
- /**
- * Create a number format that prints in a pattern we get from
- * the [getPattern] function using the locale [locale].
- */
- NumberFormat._forPattern(String locale, Function getPattern,
- [this.currencyName])
- : _locale = Intl.verifiedLocale(locale, localeExists) {
+ /// A regular expression to validate currency names are exactly three
+ /// alphabetic characters.
+ static final _checkCurrencyName = new RegExp(r'^[a-zA-Z]{3}$');
+
+ /// Create a number format that prints as CURRENCY_PATTERN. (Deprecated:
+ /// prefer NumberFormat.currency)
+ ///
+ /// If provided,
+ /// use [nameOrSymbol] in place of the default currency name. e.g.
+ /// var eurosInCurrentLocale = new NumberFormat
+ /// .currencyPattern(Intl.defaultLocale, "€");
+ @Deprecated("Use NumberFormat.currency")
+ factory NumberFormat.currencyPattern(
+ [String locale, String currencyNameOrSymbol]) {
+ // If it looks like an iso4217 name, pass as name, otherwise as symbol.
+ if (currencyNameOrSymbol != null &&
+ _checkCurrencyName.hasMatch(currencyNameOrSymbol)) {
+ return new NumberFormat.currency(
+ locale: locale, name: currencyNameOrSymbol);
+ } else {
+ return new NumberFormat.currency(
+ locale: locale, symbol: currencyNameOrSymbol);
+ }
+ }
+
+ /// Create a [NumberFormat] that formats using the locale's CURRENCY_PATTERN.
+ ///
+ /// If [locale] is not specified, it will use the current default locale.
+ ///
+ /// If [name] is specified, the currency with that ISO 4217 name will be used.
+ /// Otherwise we will use the default currency name for the current locale. If
+ /// no [symbol] is specified, we will use the currency name in the formatted
+ /// result. e.g.
+ /// var f = new NumberFormat.currency(locale: 'en_US', name: 'EUR')
+ /// will format currency like "EUR1.23". If we did not specify the name, it
+ /// would format like "USD1.23".
+ ///
+ /// If [symbol] is used, then that symbol will be used in formatting instead
+ /// of the name. e.g.
+ /// var eurosInCurrentLocale = new NumberFormat.currency(symbol: "€");
+ /// will format like "€1.23". Otherwise it will use the currency name.
+ /// If this is not explicitly specified in the constructor, then for
+ /// currencies we use the default value for the currency if the name is given,
+ /// otherwise we use the value from the pattern for the locale.
+ ///
+ /// If [decimalDigits] is specified, numbers will format with that many digits
+ /// after the decimal place. If it's not, they will use the default for the
+ /// currency in [name], and the default currency for [locale] if the currency
+ /// name is not specified. e.g.
+ /// new NumberFormat.currency(name: 'USD', decimalDigits: 7)
+ /// will format with 7 decimal digits, because that's what we asked for. But
+ /// new NumberFormat.currency(locale: 'en_US', name: 'JPY')
+ /// will format with zero, because that's the default for JPY, and the
+ /// currency's default takes priority over the locale's default.
+ /// new NumberFormat.currency(locale: 'en_US')
+ /// will format with two, which is the default for that locale.
+ // TODO(alanknight): Should we allow decimalDigits on other numbers.
+ NumberFormat.currency(
+ {String locale, String name, String symbol, int decimalDigits})
+ : this._forPattern(locale, (x) => x.CURRENCY_PATTERN,
+ name: name,
+ currencySymbol: symbol,
+ decimalDigits: decimalDigits,
+ isForCurrency: true);
+
+ /// Creates a [NumberFormat] for currencies, using the simple symbol for the
+ /// currency if one is available (e.g. $, €), so it should only be used if the
+ /// short currency symbol will be unambiguous.
+ ///
+ /// If [locale] is not specified, it will use the current default locale.
+ ///
+ /// If [name] is specified, the currency with that ISO 4217 name will be used.
+ /// Otherwise we will use the default currency name for the current locale. We
+ /// will assume that the symbol for this is well known in the locale and
+ /// unambiguous. If you format CAD in an en_US locale using this format it
+ /// will display as "$", which may be confusing to the user.
+ ///
+ /// If [decimalDigits] is specified, numbers will format with that many digits
+ /// after the decimal place. If it's not, they will use the default for the
+ /// currency in [name], and the default currency for [locale] if the currency
+ /// name is not specified. e.g.
+ /// new NumberFormat.simpleCurrency(name: 'USD', decimalDigits: 7)
+ /// will format with 7 decimal digits, because that's what we asked for. But
+ /// new NumberFormat.simpleCurrency(locale: 'en_US', name: 'JPY')
+ /// will format with zero, because that's the default for JPY, and the
+ /// currency's default takes priority over the locale's default.
+ /// new NumberFormat.simpleCurrency(locale: 'en_US')
+ /// will format with two, which is the default for that locale.
+ factory NumberFormat.simpleCurrency(
+ {String locale, String name, int decimalDigits}) {
+ return new NumberFormat._forPattern(locale, (x) => x.CURRENCY_PATTERN,
+ name: name,
+ computeCurrencySymbol: (format) =>
+ _simpleCurrencySymbols[format.currencyName] ?? format.currencyName,
+ decimalDigits: decimalDigits,
+ isForCurrency: true);
+ }
+
+ /// Returns the simple currency symbol for given currency code, or
+ /// [currencyCode] if no simple symbol is listed.
+ ///
+ /// The simple currency symbol is generally short, and the same or related to
+ /// what is used in countries having the currency as an official symbol. It
+ /// may be a symbol character, or may have letters, or both. It may be
+ /// different according to the locale: for example, for an Arabic locale it
+ /// may consist of Arabic letters, but for a French locale consist of Latin
+ /// letters. It will not be unique: for example, "$" can appear for both USD
+ /// and CAD.
+ ///
+ /// (The current implementation is the same for all locales, but this is
+ /// temporary and callers shouldn't rely on it.)
+ String simpleCurrencySymbol(String currencyCode) =>
+ _simpleCurrencySymbols[currencyCode] ?? currencyCode;
+
+ /// A map from currency names to the simple name/symbol.
+ ///
+ /// The simple currency symbol is generally short, and the same or related to
+ /// what is used in countries having the currency as an official symbol. It
+ /// may be a symbol character, or may have letters, or both. It may be
+ /// different according to the locale: for example, for an Arabic locale it
+ /// may consist of Arabic letters, but for a French locale consist of Latin
+ /// letters. It will not be unique: for example, "$" can appear for both USD
+ /// and CAD.
+ ///
+ /// (The current implementation is the same for all locales, but this is
+ /// temporary and callers shouldn't rely on it.)
+ static Map<String, String> _simpleCurrencySymbols = {
+ "AFN": "Af.",
+ "TOP": r"T$",
+ "MGA": "Ar",
+ "THB": "\u0e3f",
+ "PAB": "B/.",
+ "ETB": "Birr",
+ "VEF": "Bs",
+ "BOB": "Bs",
+ "GHS": "GHS",
+ "CRC": "\u20a1",
+ "NIO": r"C$",
+ "GMD": "GMD",
+ "MKD": "din",
+ "BHD": "din",
+ "DZD": "din",
+ "IQD": "din",
+ "JOD": "din",
+ "KWD": "din",
+ "LYD": "din",
+ "RSD": "din",
+ "TND": "din",
+ "AED": "dh",
+ "MAD": "dh",
+ "STD": "Db",
+ "BSD": r"$",
+ "FJD": r"$",
+ "GYD": r"$",
+ "KYD": r"$",
+ "LRD": r"$",
+ "SBD": r"$",
+ "SRD": r"$",
+ "AUD": r"$",
+ "BBD": r"$",
+ "BMD": r"$",
+ "BND": r"$",
+ "BZD": r"$",
+ "CAD": r"$",
+ "HKD": r"$",
+ "JMD": r"$",
+ "NAD": r"$",
+ "NZD": r"$",
+ "SGD": r"$",
+ "TTD": r"$",
+ "TWD": r"NT$",
+ "USD": r"$",
+ "XCD": r"$",
+ "VND": "\u20ab",
+ "AMD": "Dram",
+ "CVE": "CVE",
+ "EUR": "\u20ac",
+ "AWG": "Afl.",
+ "HUF": "Ft",
+ "BIF": "FBu",
+ "CDF": "FrCD",
+ "CHF": "CHF",
+ "DJF": "Fdj",
+ "GNF": "FG",
+ "RWF": "RF",
+ "XOF": "CFA",
+ "XPF": "FCFP",
+ "KMF": "CF",
+ "XAF": "FCFA",
+ "HTG": "HTG",
+ "PYG": "Gs",
+ "UAH": "\u20b4",
+ "PGK": "PGK",
+ "LAK": "\u20ad",
+ "CZK": "K\u010d",
+ "SEK": "kr",
+ "ISK": "kr",
+ "DKK": "kr",
+ "NOK": "kr",
+ "HRK": "kn",
+ "MWK": "MWK",
+ "ZMK": "ZWK",
+ "AOA": "Kz",
+ "MMK": "K",
+ "GEL": "GEL",
+ "LVL": "Ls",
+ "ALL": "Lek",
+ "HNL": "L",
+ "SLL": "SLL",
+ "MDL": "MDL",
+ "RON": "RON",
+ "BGN": "lev",
+ "SZL": "SZL",
+ "TRY": "TL",
+ "LTL": "Lt",
+ "LSL": "LSL",
+ "AZN": "man.",
+ "BAM": "KM",
+ "MZN": "MTn",
+ "NGN": "\u20a6",
+ "ERN": "Nfk",
+ "BTN": "Nu.",
+ "MRO": "MRO",
+ "MOP": "MOP",
+ "CUP": r"$",
+ "CUC": r"$",
+ "ARS": r"$",
+ "CLF": "UF",
+ "CLP": r"$",
+ "COP": r"$",
+ "DOP": r"$",
+ "MXN": r"$",
+ "PHP": "\u20b1",
+ "UYU": r"$",
+ "FKP": "£",
+ "GIP": "£",
+ "SHP": "£",
+ "EGP": "E£",
+ "LBP": "L£",
+ "SDG": "SDG",
+ "SSP": "SSP",
+ "GBP": "£",
+ "SYP": "£",
+ "BWP": "P",
+ "GTQ": "Q",
+ "ZAR": "R",
+ "BRL": r"R$",
+ "OMR": "Rial",
+ "QAR": "Rial",
+ "YER": "Rial",
+ "IRR": "Rial",
+ "KHR": "Riel",
+ "MYR": "RM",
+ "SAR": "Rial",
+ "BYR": "BYR",
+ "RUB": "руб.",
+ "MUR": "Rs",
+ "SCR": "SCR",
+ "LKR": "Rs",
+ "NPR": "Rs",
+ "INR": "\u20b9",
+ "PKR": "Rs",
+ "IDR": "Rp",
+ "ILS": "\u20aa",
+ "KES": "Ksh",
+ "SOS": "SOS",
+ "TZS": "TSh",
+ "UGX": "UGX",
+ "PEN": "S/.",
+ "KGS": "KGS",
+ "UZS": "so\u02bcm",
+ "TJS": "Som",
+ "BDT": "\u09f3",
+ "WST": "WST",
+ "KZT": "\u20b8",
+ "MNT": "\u20ae",
+ "VUV": "VUV",
+ "KPW": "\u20a9",
+ "KRW": "\u20a9",
+ "JPY": "Â¥",
+ "CNY": "Â¥",
+ "PLN": "z\u0142",
+ "MVR": "Rf",
+ "NLG": "NAf",
+ "ZMW": "ZK",
+ "ANG": "Æ’",
+ "TMT": "TMT",
+ };
+
+ /// Create a number format that prints in a pattern we get from
+ /// the [getPattern] function using the locale [locale].
+ ///
+ /// The [currencySymbol] can either be specified directly, or we can pass a
+ /// function [computeCurrencySymbol] that will compute it later, given other
+ /// information, typically the verified locale.
+ NumberFormat._forPattern(String locale, _PatternGetter getPattern,
+ {String name,
+ String currencySymbol,
+ String computeCurrencySymbol(NumberFormat),
+ int decimalDigits,
+ bool isForCurrency: false})
+ : _locale = Intl.verifiedLocale(locale, localeExists),
+ _isForCurrency = isForCurrency {
+ this._currencySymbol = currencySymbol;
+ this._decimalDigits = decimalDigits;
_symbols = numberFormatSymbols[_locale];
- if (currencyName == null) {
- currencyName = _symbols.DEF_CURRENCY_CODE;
+ _localeZero = _symbols.ZERO_DIGIT.codeUnitAt(0);
+ _zeroOffset = _localeZero - _zero;
+ _negativePrefix = _symbols.MINUS_SIGN;
+ currencyName = name ?? _symbols.DEF_CURRENCY_CODE;
+ if (this._currencySymbol == null && computeCurrencySymbol != null) {
+ this._currencySymbol = computeCurrencySymbol(this);
}
_setPattern(getPattern(_symbols));
}
- /**
- * Return the locale code in which we operate, e.g. 'en_US' or 'pt'.
- */
+ /// A number format for compact representations, e.g. "1.2M" instead
+ /// of "1,200,000".
+ factory NumberFormat.compact({String locale}) {
+ return new _CompactNumberFormat(
+ locale: locale,
+ formatType: _CompactFormatType.COMPACT_DECIMAL_SHORT_PATTERN);
+ }
+
+ /// A number format for "long" compact representations, e.g. "1.2 million"
+ /// instead of of "1,200,000".
+ factory NumberFormat.compactLong({String locale}) {
+ return new _CompactNumberFormat(
+ locale: locale,
+ formatType: _CompactFormatType.COMPACT_DECIMAL_LONG_PATTERN);
+ }
+
+ /// A number format for compact currency representations, e.g. "$1.2M" instead
+ /// of "$1,200,000", and which will automatically determine a currency symbol
+ /// based on the currency name or the locale. See
+ /// [NumberFormat.simpleCurrency].
+ factory NumberFormat.compactSimpleCurrency({String locale, String name}) {
+ return new _CompactNumberFormat(
+ locale: locale,
+ formatType: _CompactFormatType.COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN,
+ name: name,
+ getPattern: (symbols) => symbols.CURRENCY_PATTERN,
+ computeCurrencySymbol: (format) =>
+ _simpleCurrencySymbols[format.currencyName] ?? format.currencyName,
+ isForCurrency: true);
+ }
+
+ /// A number format for compact currency representations, e.g. "$1.2M" instead
+ /// of "$1,200,000".
+ factory NumberFormat.compactCurrency(
+ {String locale, String name, String symbol, int decimalDigits}) {
+ return new _CompactNumberFormat(
+ locale: locale,
+ formatType: _CompactFormatType.COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN,
+ name: name,
+ 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.
- */
+ /// 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.
- */
+ /// 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.
- */
+ /// 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}";
@@ -188,15 +571,11 @@ class NumberFormat {
return result;
}
- /**
- * Parse the number represented by the string. If it's not
- * parseable, throws a [FormatException].
- */
+ /// Parse the number represented by the string. If it's not
+ /// parseable, throws a [FormatException].
num parse(String text) => new _NumberParser(this, text).value;
- /**
- * Format the main part of the number in the form dictated by the pattern.
- */
+ /// Format the main part of the number in the form dictated by the pattern.
void _formatNumber(number) {
if (_useExponentialNotation) {
_formatExponential(number);
@@ -205,7 +584,7 @@ class NumberFormat {
}
}
- /** Format the number in exponential notation. */
+ /// Format the number in exponential notation.
void _formatExponential(num number) {
if (number == 0.0) {
_formatFixed(number);
@@ -213,10 +592,9 @@ class NumberFormat {
return;
}
- var exponent = (log(number) / log(10)).floor();
+ var exponent = (log(number) / LN10).floor();
var mantissa = number / pow(10.0, exponent);
- var minIntDigits = minimumIntegerDigits;
if (maximumIntegerDigits > 1 &&
maximumIntegerDigits > minimumIntegerDigits) {
// A repeating range is defined; adjust to it as follows.
@@ -228,7 +606,6 @@ class NumberFormat {
mantissa *= 10;
exponent--;
}
- minIntDigits = 1;
} else {
// No repeating range is defined, use minimum integer digits.
if (minimumIntegerDigits < 1) {
@@ -243,9 +620,7 @@ class NumberFormat {
_formatExponent(exponent);
}
- /**
- * Format the exponent portion, e.g. in "1.3e-5" the "e-5".
- */
+ /// Format the exponent portion, e.g. in "1.3e-5" the "e-5".
void _formatExponent(num exponent) {
_add(symbols.EXP_SYMBOL);
if (exponent < 0) {
@@ -257,28 +632,84 @@ class NumberFormat {
_pad(minimumExponentDigits, exponent.toString());
}
- /** Used to test if we have exceeded Javascript integer limits. */
+ /// Used to test if we have exceeded Javascript integer limits.
final _maxInt = pow(2, 52);
- /**
- * Helpers to check numbers that don't conform to the [num] interface,
- * e.g. Int64
- */
+ /// Helpers to check numbers that don't conform to the [num] interface,
+ /// e.g. Int64
_isInfinite(number) => number is num ? number.isInfinite : false;
_isNaN(number) => number is num ? number.isNaN : false;
- _round(number) => number is num ? number.round() : number;
- _floor(number) => number is num ? number.floor() : number;
- /**
- * Format the basic number portion, inluding the fractional digits.
- */
+ /// Helper to get the floor of a number which might not be num. This should
+ /// only ever be called with an argument which is positive, or whose abs()
+ /// is negative. The second case is the maximum negative value on a
+ /// fixed-length integer. Since they are integers, they are also their own
+ /// floor.
+ _floor(number) {
+ if (number.isNegative && !(number.abs().isNegative)) {
+ throw new ArgumentError(
+ "Internal error: expected positive number, got $number");
+ }
+ return (number is num) ? number.floor() : number ~/ 1;
+ }
+
+ /// Helper to round a number which might not be num.
+ _round(number) {
+ if (number is num) {
+ if (number.isInfinite) {
+ return _maxInt;
+ } else {
+ return number.round();
+ }
+ } else if (number.remainder(1) == 0) {
+ // Not a normal number, but int-like, e.g. Int64
+ return number;
+ } else {
+ // TODO(alanknight): Do this more efficiently. If IntX had floor and round we could avoid this.
+ var basic = _floor(number);
+ var fraction = (number - basic).toDouble().round();
+ return fraction == 0 ? number : number + fraction;
+ }
+ }
+
+ // Return the number of digits left of the decimal place in [number].
+ static int numberOfIntegerDigits(number) {
+ var simpleNumber = number.toDouble().abs();
+ // It's unfortunate that we have to do this, but we get precision errors
+ // that affect the result if we use logs, e.g. 1000000
+ if (simpleNumber < 10) return 1;
+ if (simpleNumber < 100) return 2;
+ if (simpleNumber < 1000) return 3;
+ if (simpleNumber < 10000) return 4;
+ if (simpleNumber < 100000) return 5;
+ if (simpleNumber < 1000000) return 6;
+ if (simpleNumber < 10000000) return 7;
+ if (simpleNumber < 100000000) return 8;
+ if (simpleNumber < 1000000000) return 9;
+ if (simpleNumber < 10000000000) return 10;
+ if (simpleNumber < 100000000000) return 11;
+ if (simpleNumber < 1000000000000) return 12;
+ if (simpleNumber < 10000000000000) return 13;
+ if (simpleNumber < 100000000000000) return 14;
+ if (simpleNumber < 1000000000000000) return 15;
+ if (simpleNumber < 10000000000000000) return 16;
+ // We're past the point where being off by one on the number of digits
+ // will affect the pattern, so now we can use logs.
+ return max(1, (log(simpleNumber) / LN10).ceil());
+ }
+
+ int _fractionDigitsAfter(int remainingSignificantDigits) =>
+ max(0, remainingSignificantDigits);
+
+ /// Format the basic number portion, including the fractional digits.
void _formatFixed(number) {
var integerPart;
int fractionPart;
int extraIntegerDigits;
+ var fractionDigits = maximumFractionDigits;
- final power = pow(10, maximumFractionDigits);
- final digitMultiplier = power * _multiplier;
+ var power = 0;
+ var digitMultiplier;
if (_isInfinite(number)) {
integerPart = number.toInt();
@@ -293,6 +724,23 @@ class NumberFormat {
// integer pieces.
integerPart = _floor(number);
var fraction = number - integerPart;
+
+ /// If we have significant digits, recalculate the number of fraction
+ /// digits based on that.
+ if (significantDigitsInUse) {
+ var integerLength = numberOfIntegerDigits(integerPart);
+ var remainingSignificantDigits =
+ significantDigits - _multiplierDigits - integerLength;
+ fractionDigits = _fractionDigitsAfter(remainingSignificantDigits);
+ if (remainingSignificantDigits < 0) {
+ // We may have to round.
+ var divideBy = pow(10, integerLength - significantDigits);
+ integerPart = (integerPart / divideBy).round() * divideBy;
+ }
+ }
+ power = pow(10, fractionDigits);
+ digitMultiplier = power * _multiplier;
+
// Multiply out to the number of decimal places and the percent, then
// round. For fixed-size integer types this should always be zero, so
// multiplying is OK.
@@ -306,13 +754,14 @@ class NumberFormat {
extraIntegerDigits = remainingDigits ~/ power;
fractionPart = remainingDigits % power;
}
- var fractionPresent = minimumFractionDigits > 0 || fractionPart > 0;
var integerDigits = _integerDigits(integerPart, extraIntegerDigits);
var digitLength = integerDigits.length;
+ var fractionPresent =
+ fractionDigits > 0 && (minimumFractionDigits > 0 || fractionPart > 0);
if (_hasIntegerDigits(integerDigits)) {
- _pad(minimumIntegerDigits - digitLength);
+ _padEmpty(minimumIntegerDigits - digitLength);
for (var i = 0; i < digitLength; i++) {
_addDigit(integerDigits.codeUnitAt(i));
_group(digitLength, i);
@@ -326,10 +775,8 @@ class NumberFormat {
_formatFractionPart((fractionPart + power).toString());
}
- /**
- * Compute the raw integer digits which will then be printed with
- * grouping and translated to localized digits.
- */
+ /// Compute the raw integer digits which will then be printed with
+ /// grouping and translated to localized digits.
String _integerDigits(integerPart, extraIntegerDigits) {
// If the int part is larger than 2^52 and we're on Javascript (so it's
// really a float) it will lose precision, so pad out the rest of it
@@ -338,7 +785,7 @@ class NumberFormat {
if (1 is double && integerPart is num && integerPart > _maxInt) {
var howManyDigitsTooBig = (log(integerPart) / LN10).ceil() - 16;
var divisor = pow(10, howManyDigitsTooBig).round();
- paddingDigits = symbols.ZERO_DIGIT * howManyDigitsTooBig.toInt();
+ paddingDigits = '0' * howManyDigitsTooBig.toInt();
integerPart = (integerPart / divisor).truncate();
}
@@ -349,85 +796,90 @@ class NumberFormat {
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.
- */
+ /// 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.
- */
+ /// Format the part after the decimal place in a fixed point number.
void _formatFractionPart(String fractionPart) {
- var fractionCodes = fractionPart.codeUnits;
var fractionLength = fractionPart.length;
- while (fractionCodes[fractionLength - 1] == _zero &&
+ while (fractionPart.codeUnitAt(fractionLength - 1) == _zero &&
fractionLength > minimumFractionDigits + 1) {
fractionLength--;
}
for (var i = 1; i < fractionLength; i++) {
- _addDigit(fractionCodes[i]);
+ _addDigit(fractionPart.codeUnitAt(i));
}
}
- /** Print the decimal separator if appropriate. */
+ /// 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.
- */
+ /// 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.
- */
+ /// 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 _addCharCode(int x) {
- _buffer.writeCharCode(x);
- }
+
void _addZero() {
_buffer.write(symbols.ZERO_DIGIT);
}
+
void _addDigit(int x) {
- _buffer.writeCharCode(_localeZero + x - _zero);
+ _buffer.writeCharCode(x + _zeroOffset);
}
- /** Print padding up to [numberOfDigits] above what's included in [basic]. */
- void _pad(int numberOfDigits, [String basic = '']) {
+ void _padEmpty(int howMany) {
+ _buffer.write(symbols.ZERO_DIGIT * howMany);
+ }
+
+ 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 x in basic.codeUnits) {
- _addDigit(x);
+ for (int 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].
- */
+ /// 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;
@@ -439,171 +891,166 @@ class NumberFormat {
}
}
- /** Returns the code point for the character '0'. */
- final _zero = '0'.codeUnits.first;
+ /// The code point for the character '0'.
+ static const _zero = 48;
- /** Returns the code point for the locale's zero digit. */
- // Note that there is a slight risk of a locale's zero digit not fitting
- // into a single code unit, but it seems very unlikely, and if it did,
- // there's a pretty good chance that our assumptions about being able to do
- // arithmetic on it would also be invalid.
- get _localeZero => symbols.ZERO_DIGIT.codeUnits.first;
+ /// The code point for the locale's zero digit.
+ ///
+ /// Initialized when the locale is set.
+ int _localeZero = 0;
- /**
- * Returns the prefix for [x] based on whether it's positive or negative.
- * In en_US this would be '' and '-' respectively.
- */
+ /// 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.
- */
+ /// Returns the suffix for [x] based on wether it's positive or negative.
+ /// In en_US there are no suffixes for positive or negative.
String _signSuffix(x) => x.isNegative ? _negativeSuffix : _positiveSuffix;
void _setPattern(String newPattern) {
if (newPattern == null) return;
// Make spaces non-breaking
_pattern = newPattern.replaceAll(' ', '\u00a0');
- var parser = new _NumberFormatParser(this, newPattern, currencyName);
+ var parser = new _NumberFormatParser(
+ this, newPattern, currencySymbol, decimalDigits);
parser.parse();
+ if (_overridesDecimalDigits) {
+ _decimalDigits ??= _defaultDecimalDigits;
+ minimumFractionDigits = _decimalDigits;
+ maximumFractionDigits = _decimalDigits;
+ }
+ }
+
+ /// Explicitly turn off any grouping (e.g. by thousands) in this format.
+ ///
+ /// This is used in compact number formatting, where we
+ /// omit the normal grouping. Best to know what you're doing if you call it.
+ void turnOffGrouping() {
+ _groupingSize = 0;
+ _finalGroupingSize = 0;
}
String toString() => "NumberFormat($_locale, $_pattern)";
}
-/**
- * A one-time object for parsing a particular numeric string. One-time here
- * means an instance can only parse one string. This is implemented by
- * transforming from a locale-specific format to one that the system can parse,
- * then calls the system parsing methods on it.
- */
+/// 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. */
+ /// The format for which we are parsing.
final NumberFormat format;
- /** The text we are parsing. */
+ /// The text we are parsing.
final String text;
- /** What we use to iterate over the input text. */
+ /// What we use to iterate over the input text.
final _Stream input;
- /**
- * The result of parsing [text] according to [format]. Automatically
- * populated in the constructor.
- */
+ /// The result of parsing [text] according to [format]. Automatically
+ /// populated in the constructor.
num value;
- /** The symbols used by our format. */
+ /// The symbols used by our format.
NumberSymbols get symbols => format.symbols;
- /** Where we accumulate the normalized representation of the number. */
+ /// Where we accumulate the normalized representation of the number.
final StringBuffer _normalized = new StringBuffer();
- /**
- * Did we see something that indicates this is, or at least might be,
- * a positive number.
- */
+ /// 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.
- */
+ /// 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].
- */
+
+ /// 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].
- */
+
+ /// 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. */
+ /// Should we stop parsing before hitting the end of the string.
bool done = false;
- /** Have we already skipped over any required prefixes. */
+ /// 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. */
+ /// If the number is percent or permill, what do we divide by at the end.
int scale = 1;
String get _positivePrefix => format._positivePrefix;
String get _negativePrefix => format._negativePrefix;
String get _positiveSuffix => format._positiveSuffix;
String get _negativeSuffix => format._negativeSuffix;
- int get _zero => format._zero;
+ int get _zero => NumberFormat._zero;
int get _localeZero => format._localeZero;
- /**
- * Create a new [_NumberParser] on which we can call parse().
- */
+ /// Create a new [_NumberParser] on which we can call parse().
_NumberParser(this.format, text)
: this.text = text,
this.input = new _Stream(text) {
+ scale = format._internalMultiplier;
value = parse();
}
- /**
- * The strings we might replace with functions that return the replacement
- * values. They are functions because we might need to check something
- * in the context. Note that the ordering is important here. For example,
- * [symbols.PERCENT] might be " %", and we must handle that before we
- * look at an individual space.
- */
- Map<String, Function> get replacements => _replacements == null
- ? _replacements = _initializeReplacements()
- : _replacements;
-
- var _replacements;
-
- Map _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,
- '+': () => '+',
- '-': () => '-',
- };
+ /// The strings we might replace with functions that return the replacement
+ /// values. They are functions because we might need to check something
+ /// in the context. Note that the ordering is important here. For example,
+ /// [symbols.PERCENT] might be " %", and we must handle that before we
+ /// look at an individual space.
+ Map<String, Function> get replacements =>
+ _replacements ??= _initializeReplacements();
+
+ Map<String, Function> _replacements;
+
+ Map<String, Function> _initializeReplacements() => {
+ symbols.DECIMAL_SEP: () => '.',
+ symbols.EXP_SYMBOL: () => 'E',
+ symbols.GROUP_SEP: handleSpace,
+ symbols.PERCENT: () {
+ scale = _NumberFormatParser._PERCENT_SCALE;
+ return '';
+ },
+ symbols.PERMILL: () {
+ scale = _NumberFormatParser._PER_MILLE_SCALE;
+ return '';
+ },
+ ' ': handleSpace,
+ '\u00a0': handleSpace,
+ '+': () => '+',
+ '-': () => '-',
+ };
invalidFormat() =>
throw new FormatException("Invalid number: ${input.contents}");
- /**
- * Replace a space in the number with the normalized form. If space is not
- * a significant character (normally grouping) then it's just invalid. If it
- * is the grouping character, then it's only valid if it's followed by a
- * digit. e.g. '$12 345.00'
- */
+ /// Replace a space in the number with the normalized form. If space is not
+ /// a significant character (normally grouping) then it's just invalid. If it
+ /// is the grouping character, then it's only valid if it's followed by a
+ /// digit. e.g. '$12 345.00'
handleSpace() =>
groupingIsNotASpaceOrElseItIsSpaceFollowedByADigit ? '' : invalidFormat();
- /**
- * Determine if a space is a valid character in the number. See [handleSpace].
- */
+ /// 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.
- */
+ /// 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;
@@ -614,25 +1061,20 @@ class _NumberParser {
}
}
- /**
- * Check to see if the input begins with either the positive or negative
- * prefixes. Set the [gotPositive] and [gotNegative] variables accordingly.
- */
+ /// Check to see if the input begins with either the positive or negative
+ /// prefixes. Set the [gotPositive] and [gotNegative] variables accordingly.
void checkPrefixes({bool skip: false}) {
- bool checkPrefix(String prefix, skip) {
- var matched = prefix.isNotEmpty && input.startsWith(prefix);
- if (skip && matched) input.read(prefix.length);
- return matched;
- }
+ 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, skip)) gotPositive = true;
- if (checkPrefix(_negativePrefix, skip)) gotNegative = true;
+ if (checkPrefix(_positivePrefix)) gotPositive = true;
+ if (checkPrefix(_negativePrefix)) gotNegative = true;
- // Copied from Closure. It doesn't seem to be necessary to pass the test
- // suite, so I'm not sure it's really needed.
+ // 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;
@@ -640,25 +1082,35 @@ class _NumberParser {
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.
- */
+ /// 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.
- */
+ /// 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]());
@@ -666,21 +1118,14 @@ class _NumberParser {
return;
}
}
- // 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.
- if (input.index == 0 && !prefixesSkipped) {
- prefixesSkipped = true;
- checkPrefixes(skip: true);
- } else {
+ // 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.
- */
+ /// Parse [text] and return the resulting number. Throws [FormatException]
+ /// if we can't parse it.
num parse() {
if (text == symbols.NAN) return double.NAN;
if (text == "$_positivePrefix${symbols.INFINITY}$_positiveSuffix") {
@@ -700,15 +1145,16 @@ class _NumberParser {
return parsed;
}
- /** The number is invalid, throw a [FormatException]. */
+ /// The number is invalid, throw a [FormatException].
void invalidNumber() =>
throw new FormatException("Invalid Number: ${input.contents}");
- /**
- * Parse the number portion of the input, i.e. not any prefixes or suffixes,
- * and assuming NaN and Infinity are already handled.
- */
+ /// Parse the number portion of the input, i.e. not any prefixes or suffixes,
+ /// and assuming NaN and Infinity are already handled.
num parseNumber(_Stream input) {
+ if (gotNegative) {
+ _normalized.write('-');
+ }
while (!done && !input.atEnd()) {
int digit = asDigit(input.peek());
if (digit != null) {
@@ -727,18 +1173,13 @@ class _NumberParser {
}
}
-/**
- * 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.
- */
+/// 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.
- */
+ /// The special characters in the pattern language. All others are treated
+ /// as literals.
static const _PATTERN_SEPARATOR = ';';
static const _QUOTE = "'";
static const _PATTERN_DIGIT = '#';
@@ -753,28 +1194,31 @@ class _NumberFormatParser {
static const _PATTERN_EXPONENT = 'E';
static const _PATTERN_PLUS = '+';
- /** The format whose state we are setting. */
+ /// The format whose state we are setting.
final NumberFormat format;
- /** The pattern we are parsing. */
+ /// The pattern we are parsing.
final _StringIterator pattern;
- /** We can be passed a specific currency symbol, regardless of the locale. */
- String currencyName;
+ /// 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.currencyName)
+ /// 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. */
+ /// The [NumberSymbols] for the locale in which our [format] prints.
NumberSymbols get symbols => format.symbols;
- /** Parse the input pattern and set the values. */
+ /// Parse the input pattern and set the values.
void parse() {
format._positivePrefix = _parseAffix();
var trunk = _parseTrunk();
@@ -801,16 +1245,12 @@ class _NumberFormatParser {
}
}
- /**
- * Variable used in parsing prefixes and suffixes to keep track of
- * whether or not we are in a quoted region.
- */
+ /// 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].
- */
+ /// Parse a prefix or suffix and return the prefix/suffix string. Note that
+ /// this also may modify the state of [format].
String _parseAffix() {
var affix = new StringBuffer();
inQuote = false;
@@ -818,11 +1258,9 @@ class _NumberFormatParser {
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.
- */
+ /// 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;
@@ -848,7 +1286,7 @@ class _NumberFormatParser {
return false;
case _PATTERN_CURRENCY_SIGN:
// TODO(alanknight): Handle the local/global/portable currency signs
- affix.write(currencyName);
+ affix.write(currencySymbol);
break;
case _PATTERN_PERCENT:
if (format._multiplier != 1 && format._multiplier != _PERCENT_SCALE) {
@@ -872,17 +1310,15 @@ class _NumberFormatParser {
return true;
}
- /** Variables used in [_parseTrunk] and [parseTrunkCharacter]. */
+ /// 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.
- */
+ /// Parse the "trunk" portion of the pattern, the piece that doesn't include
+ /// positive or negative prefixes or suffixes.
String _parseTrunk() {
var loop = true;
var trunk = new StringBuffer();
@@ -945,11 +1381,9 @@ class _NumberFormatParser {
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.
- */
+ /// 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) {
@@ -1027,31 +1461,23 @@ class _NumberFormatParser {
}
}
-/**
- * Returns an [Iterable] on the string as a list of substrings.
- */
+/// Returns an [Iterable] on the string as a list of substrings.
Iterable _iterable(String s) => new _StringIterable(s);
-/**
- * Return an iterator on the string as a list of substrings.
- */
-Iterator _iterator(String s) => new _StringIterator(s);
+/// Return an iterator on the string as a list of substrings.
+Iterator<String> _iterator(String s) => new _StringIterator(s);
// TODO(nweiz): remove this when issue 3780 is fixed.
-/**
- * Provides an Iterable that wraps [_iterator] so it can be used in a `for`
- * loop.
- */
+/// 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.
- */
+/// 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;
@@ -1079,3 +1505,82 @@ class _StringIterator implements Iterator<String> {
return input;
}
}
+
+/// Used primarily for currency formatting, this number-like class stores
+/// millionths of a currency unit, typically as an Int64.
+///
+/// It supports no operations other than being used for Intl number formatting.
+abstract class MicroMoney {
+ factory MicroMoney(micros) => new _MicroMoney(micros);
+}
+
+/// Used primarily for currency formatting, this stores millionths of a
+/// currency unit, typically as an Int64.
+///
+/// This private class provides the operations needed by the formatting code.
+class _MicroMoney implements MicroMoney {
+ var _micros;
+ _MicroMoney(this._micros);
+ static const _multiplier = 1000000;
+
+ get _integerPart => _micros ~/ _multiplier;
+ int get _fractionPart => (this - _integerPart)._micros.toInt().abs();
+
+ bool get isNegative => _micros.isNegative;
+
+ _MicroMoney abs() => isNegative ? new _MicroMoney(_micros.abs()) : this;
+
+ // Note that if this is done in a general way there's a risk of integer
+ // overflow on JS when multiplying out the [other] parameter, which may be
+ // an Int64. In formatting we only ever subtract out our own integer part.
+ _MicroMoney operator -(other) {
+ if (other is _MicroMoney) return new _MicroMoney(_micros - other._micros);
+ return new _MicroMoney(_micros - (other * _multiplier));
+ }
+
+ _MicroMoney operator +(other) {
+ if (other is _MicroMoney) return new _MicroMoney(_micros + other._micros);
+ return new _MicroMoney(_micros + (other * _multiplier));
+ }
+
+ _MicroMoney operator ~/(divisor) {
+ if (divisor is! int) {
+ throw new ArgumentError.value(
+ divisor, 'divisor', '_MicroMoney ~/ only supports int arguments.');
+ }
+ return new _MicroMoney((_integerPart ~/ divisor) * _multiplier);
+ }
+
+ _MicroMoney operator *(other) {
+ if (other is! int) {
+ throw new ArgumentError.value(
+ other, 'other', '_MicroMoney * only supports int arguments.');
+ }
+ return new _MicroMoney(
+ (_integerPart * other) * _multiplier + (_fractionPart * other));
+ }
+
+ /// Note that this only really supports remainder from an int,
+ /// not division by another MicroMoney
+ _MicroMoney remainder(other) {
+ if (other is! int) {
+ throw new ArgumentError.value(
+ other, 'other', '_MicroMoney.remainder only supports int arguments.');
+ }
+ return new _MicroMoney(_micros.remainder(other * _multiplier));
+ }
+
+ double toDouble() => _micros.toDouble() / _multiplier;
+
+ int toInt() => _integerPart.toInt();
+
+ String toString() {
+ var beforeDecimal = _integerPart.toString();
+ var decimalPart = '';
+ var fractionPart = _fractionPart;
+ if (fractionPart != 0) {
+ decimalPart = '.' + fractionPart.toString();
+ }
+ return '$beforeDecimal$decimalPart';
+ }
+}
« no previous file with comments | « packages/intl/lib/src/intl/date_format_helpers.dart ('k') | packages/intl/lib/src/intl_helpers.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698