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