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