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'; |
+ } |
+} |