| Index: packages/intl/lib/src/intl/compact_number_format.dart
|
| diff --git a/packages/intl/lib/src/intl/compact_number_format.dart b/packages/intl/lib/src/intl/compact_number_format.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..ef4f3322158edfdc15f8a5f1374e2a1f147a2f8d
|
| --- /dev/null
|
| +++ b/packages/intl/lib/src/intl/compact_number_format.dart
|
| @@ -0,0 +1,280 @@
|
| +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
|
| +// for details. All rights reserved. Use of this source code is governed by a
|
| +// BSD-style license that can be found in the LICENSE file.
|
| +
|
| +part of intl;
|
| +
|
| +/// Represents a compact format for a particular base
|
| +///
|
| +/// For example, 10k can be used to represent 10,000. Corresponds to one of the
|
| +/// patterns in COMPACT_DECIMAL_SHORT_FORMAT. So, for example, in en_US we have
|
| +/// the pattern
|
| +///
|
| +/// 4: '0K'
|
| +/// which matches
|
| +///
|
| +/// new _CompactStyle(pattern: '0K', requiredDigits: 4, divisor: 1000,
|
| +/// expectedDigits: 1, prefix: '', suffix: 'K');
|
| +///
|
| +/// where expectedDigits is the number of zeros.
|
| +class _CompactStyle {
|
| + _CompactStyle(
|
| + {this.pattern,
|
| + this.requiredDigits: 0,
|
| + this.divisor: 1,
|
| + this.expectedDigits: 1,
|
| + this.prefix: '',
|
| + this.suffix: ''});
|
| +
|
| + /// The pattern on which this is based.
|
| + ///
|
| + /// We don't actually need this, but it makes debugging easier.
|
| + String pattern;
|
| +
|
| + /// The length for which the format applies.
|
| + ///
|
| + /// So if this is 3, we expect it to apply to numbers from 100 up. Typically
|
| + /// it would be from 100 to 1000, but that depends if there's a style for 4 or
|
| + /// not. This is the CLDR index of the pattern, and usually determines the
|
| + /// divisor, but if the pattern is just a 0 with no prefix or suffix then we
|
| + /// don't divide at all.
|
| + int requiredDigits;
|
| +
|
| + /// What should we divide the number by in order to print. Normally is either
|
| + /// 10^requiredDigits or 1 if we shouldn't divide at all.
|
| + int divisor;
|
| +
|
| + /// How many integer digits do we expect to print - the number of zeros in the
|
| + /// CLDR pattern.
|
| + int expectedDigits;
|
| +
|
| + /// Text we put in front of the number part.
|
| + String prefix;
|
| +
|
| + /// Text we put after the number part.
|
| + String suffix;
|
| +
|
| + /// How many total digits do we expect in the number.
|
| + ///
|
| + /// If the pattern is
|
| + ///
|
| + /// 4: "00K",
|
| + ///
|
| + /// then this is 5, meaning we expect this to be a 5-digit (or more)
|
| + /// number. We will scale by 1000 and expect 2 integer digits remaining, so we
|
| + /// get something like '12K'. This is used to find the closest pattern for a
|
| + /// number.
|
| + get totalDigits => requiredDigits + expectedDigits - 1;
|
| +
|
| + /// Return true if this is the fallback compact pattern, printing the number
|
| + /// un-compacted. e.g. 1200 might print as "1.2K", but 12 just prints as "12".
|
| + ///
|
| + /// For currencies, with the fallback pattern we use the super implementation
|
| + /// so that we will respect things like the default number of decimal digits
|
| + /// for a particular currency (e.g. two for USD, zero for JPY)
|
| + bool get isFallback => pattern == null || pattern == '0';
|
| +
|
| + /// Should we print the number as-is, without dividing.
|
| + ///
|
| + /// This happens if the pattern has no abbreviation for scaling (e.g. K, M).
|
| + /// So either the pattern is empty or it is of a form like '0 $'. This is a
|
| + /// workaround for locales like "it", which include patterns with no suffix
|
| + /// for numbers >= 1000 but < 1,000,000.
|
| + bool get printsAsIs =>
|
| + isFallback ||
|
| + pattern.replaceAll(new RegExp('[0\u00a0\u00a4]'), '').isEmpty;
|
| +}
|
| +
|
| +enum _CompactFormatType {
|
| + COMPACT_DECIMAL_SHORT_PATTERN,
|
| + COMPACT_DECIMAL_LONG_PATTERN,
|
| + COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN
|
| +}
|
| +
|
| +class _CompactNumberFormat extends NumberFormat {
|
| + /// A default, using the decimal pattern, for the [getPattern] constructor parameter.
|
| + static String _forDecimal(NumberSymbols symbols) => symbols.DECIMAL_PATTERN;
|
| +
|
| + // Will be either the COMPACT_DECIMAL_SHORT_PATTERN,
|
| + // COMPACT_DECIMAL_LONG_PATTERN, or COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN
|
| + Map<int, String> _patterns;
|
| +
|
| + List<_CompactStyle> _styles = [];
|
| +
|
| + _CompactNumberFormat(
|
| + {String locale,
|
| + _CompactFormatType formatType,
|
| + String name,
|
| + String currencySymbol,
|
| + String getPattern(NumberSymbols symbols): _forDecimal,
|
| + String computeCurrencySymbol(NumberFormat),
|
| + int decimalDigits,
|
| + bool isForCurrency: false})
|
| + : super._forPattern(locale, getPattern,
|
| + name: name,
|
| + currencySymbol: currencySymbol,
|
| + computeCurrencySymbol: computeCurrencySymbol,
|
| + decimalDigits: decimalDigits,
|
| + isForCurrency: isForCurrency) {
|
| + significantDigits = 3;
|
| + turnOffGrouping();
|
| + switch (formatType) {
|
| + case _CompactFormatType.COMPACT_DECIMAL_SHORT_PATTERN:
|
| + _patterns = compactSymbols.COMPACT_DECIMAL_SHORT_PATTERN;
|
| + break;
|
| + case _CompactFormatType.COMPACT_DECIMAL_LONG_PATTERN:
|
| + _patterns = compactSymbols.COMPACT_DECIMAL_LONG_PATTERN ??
|
| + compactSymbols.COMPACT_DECIMAL_SHORT_PATTERN;
|
| + break;
|
| + case _CompactFormatType.COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN:
|
| + _patterns = compactSymbols.COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN;
|
| + break;
|
| + default:
|
| + throw new ArgumentError.notNull("formatType");
|
| + }
|
| + var regex = new RegExp('([^0]*)(0+)(.*)');
|
| + _patterns.forEach((int impliedDigits, String pattern) {
|
| + var match = regex.firstMatch(pattern);
|
| + var integerDigits = match.group(2).length;
|
| + var prefix = match.group(1);
|
| + var suffix = match.group(3);
|
| + // If the pattern is just zeros, with no suffix, then we shouldn't divide
|
| + // by the number of digits. e.g. for 'af', the pattern for 3 is '0', but
|
| + // it doesn't mean that 4321 should print as 4. But if the pattern was
|
| + // '0K', then it should print as '4K'. So we have to check if the pattern
|
| + // has a suffix. This seems extremely hacky, but I don't know how else to
|
| + // encode that. Check what other things are doing.
|
| + var divisor = 1;
|
| + if (pattern.replaceAll('0', '').isNotEmpty) {
|
| + divisor = pow(10, impliedDigits - integerDigits + 1);
|
| + }
|
| + var style = new _CompactStyle(
|
| + pattern: pattern,
|
| + requiredDigits: impliedDigits,
|
| + expectedDigits: integerDigits,
|
| + prefix: prefix,
|
| + suffix: suffix,
|
| + divisor: divisor);
|
| + _styles.add(style);
|
| + });
|
| + // Reverse the styles so that we look through them from largest to smallest.
|
| + _styles = _styles.reversed.toList();
|
| + // Add a fallback style that just prints the number.
|
| + _styles.add(new _CompactStyle());
|
| + }
|
| +
|
| + /// The style in which we will format a particular number.
|
| + ///
|
| + /// This is a temporary variable that is only valid within a call to format.
|
| + _CompactStyle _style;
|
| +
|
| + String format(number) {
|
| + _style = _styleFor(number);
|
| + var divisor = _style.printsAsIs ? 1 : _style.divisor;
|
| + var numberToFormat = _divide(number, divisor);
|
| + var formatted = super.format(numberToFormat);
|
| + var prefix = _style.prefix;
|
| + var suffix = _style.suffix;
|
| + // If this is for a currency, then the super call will have put the currency
|
| + // somewhere. We don't want it there, we want it where our style indicates,
|
| + // so remove it and replace. This has the remote possibility of a false
|
| + // positive, but it seems unlikely that e.g. USD would occur as a string in
|
| + // a regular number.
|
| + if (this._isForCurrency && !_style.isFallback) {
|
| + formatted = formatted.replaceFirst(currencySymbol, '').trim();
|
| + prefix = prefix.replaceFirst('\u00a4', currencySymbol);
|
| + suffix = suffix.replaceFirst('\u00a4', currencySymbol);
|
| + }
|
| + var withExtras = "${prefix}$formatted${suffix}";
|
| + _style = null;
|
| + return withExtras;
|
| + }
|
| +
|
| + /// How many digits after the decimal place should we display, given that
|
| + /// there are [remainingSignificantDigits] left to show.
|
| + int _fractionDigitsAfter(int remainingSignificantDigits) {
|
| + var newFractionDigits =
|
| + super._fractionDigitsAfter(remainingSignificantDigits);
|
| + // For non-currencies, or for currencies if the numbers are large enough to
|
| + // compact, always use the number of significant digits and ignore
|
| + // decimalDigits. That is, $1.23K but also ¥12.3\u4E07, even though yen
|
| + // don't normally print decimal places.
|
| + if (!_isForCurrency || !_style.isFallback) return newFractionDigits;
|
| + // If we are printing a currency and it's too small to compact, but
|
| + // significant digits would have us only print some of the decimal digits,
|
| + // use all of them. So $12.30, not $12.3
|
| + if (newFractionDigits > 0 && newFractionDigits < decimalDigits) {
|
| + return decimalDigits;
|
| + } else {
|
| + return min(newFractionDigits, decimalDigits);
|
| + }
|
| + }
|
| +
|
| + /// Divide numbers that may not have a division operator (e.g. Int64).
|
| + ///
|
| + /// Only used for powers of 10, so we require an integer denominator.
|
| + num _divide(numerator, int denominator) {
|
| + if (numerator is num) {
|
| + return numerator / denominator;
|
| + }
|
| + // If it doesn't fit in a JS int after division, we're not going to be able
|
| + // to meaningfully print a compact representation for it.
|
| + var divided = numerator ~/ denominator;
|
| + var integerPart = divided.toInt();
|
| + if (divided != integerPart) {
|
| + throw new FormatException(
|
| + "Number too big to use with compact format", numerator);
|
| + }
|
| + var remainder = numerator.remainder(denominator).toInt();
|
| + var originalFraction = numerator - (numerator ~/ 1);
|
| + var fraction = originalFraction == 0 ? 0 : originalFraction / denominator;
|
| + return integerPart + (remainder / denominator) + fraction;
|
| + }
|
| +
|
| + _CompactStyle _styleFor(number) {
|
| + // We have to round the number based on the number of significant digits so
|
| + // that we pick the right style based on the rounded form and format 999999
|
| + // as 1M rather than 1000K.
|
| + var originalLength = NumberFormat.numberOfIntegerDigits(number);
|
| + var additionalDigits = originalLength - significantDigits;
|
| + var digitLength = originalLength;
|
| + if (additionalDigits > 0) {
|
| + var divisor = pow(10, additionalDigits);
|
| + // If we have an Int64, value speed over precision and make it double.
|
| + var rounded = (number.toDouble() / divisor).round() * divisor;
|
| + digitLength = NumberFormat.numberOfIntegerDigits(rounded);
|
| + }
|
| + for (var style in _styles) {
|
| + if (digitLength > style.totalDigits) {
|
| + return style;
|
| + }
|
| + }
|
| + throw new FormatException(
|
| + "No compact style found for number. This should not happen", number);
|
| + }
|
| +
|
| + num parse(String text) {
|
| + for (var style in _styles.reversed) {
|
| + if (text.startsWith(style.prefix) && text.endsWith(style.suffix)) {
|
| + var numberText = text.substring(
|
| + style.prefix.length, text.length - style.suffix.length);
|
| + var number = _tryParsing(numberText);
|
| + if (number != null) {
|
| + return number * style.divisor;
|
| + }
|
| + }
|
| + }
|
| + throw new FormatException(
|
| + "Cannot parse compact number in locale '$locale'", text);
|
| + }
|
| +
|
| + num _tryParsing(String text) {
|
| + try {
|
| + return super.parse(text);
|
| + } on FormatException {
|
| + return null;
|
| + }
|
| + }
|
| +
|
| + CompactNumberSymbols get compactSymbols => compactNumberSymbols[_locale];
|
| +}
|
|
|