blob: ceb13d13875064a06c6ee79b8a60dc3e84908108 [file] [log] [blame]
// 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.
// @dart=2.9
part of 'number_format.dart';
// Suppress naming issues as changes would be breaking.
// ignore_for_file: constant_identifier_names
/// An abstract class for compact number styles.
abstract class _CompactStyleBase {
/// The _CompactStyle for the sign of [number], i.e. positive or
/// negative.
_CompactStyle styleForSign(number);
/// 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.
int get totalDigits;
/// What should we divide the number by in order to print. Normally it is
/// either `10^normalizedExponent` or 1 if we shouldn't divide at all.
int get divisor;
/// The iterable of all possible styles which we represent.
///
/// Normally this will be either a list with just ourself, or of two elements
/// for our positive and negative styles.
Iterable<_CompactStyle> get allStyles;
}
/// A compact format with separate styles for positive and negative numbers.
class _CompactStyleWithNegative extends _CompactStyleBase {
_CompactStyleWithNegative(this.positiveStyle, this.negativeStyle);
final _CompactStyle positiveStyle;
final _CompactStyle negativeStyle;
_CompactStyle styleForSign(number) =>
number < 0 ? negativeStyle : positiveStyle;
int get totalDigits => positiveStyle.totalDigits;
int get divisor => positiveStyle.divisor;
List<_CompactStyle> get allStyles => [positiveStyle, negativeStyle];
}
/// 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: '00K'
/// which matches
///
/// _CompactStyle(pattern: '00K', normalizedExponent: 4, divisor: 1000,
/// expectedDigits: 1, prefix: '', suffix: 'K');
///
/// where expectedDigits is the number of zeros.
class _CompactStyle extends _CompactStyleBase {
_CompactStyle(
{this.pattern,
this.normalizedExponent = 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 normalized scientific notation exponent for which the format applies.
///
/// So if this is 3, we expect it to apply for numbers from 1000 and up.
/// Typically it would be from 1000 to 9999, 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 normalizedExponent;
/// What should we divide the number by in order to print. Normally is either
/// 10^normalizedExponent 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.
int get totalDigits => normalizedExponent + 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(RegExp('[0\u00a0\u00a4]'), '').isEmpty;
_CompactStyle styleForSign(number) => this;
List<_CompactStyle> get allStyles => [this];
}
/// Enumerates the different formats supported.
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;
List<_CompactStyleBase> _styles = [];
_CompactNumberFormat(
{String locale,
_CompactFormatType formatType,
String name,
String currencySymbol,
String Function(NumberSymbols) getPattern = _forDecimal,
int decimalDigits,
bool lookupSimpleCurrencySymbol = false,
bool isForCurrency = false})
: super._forPattern(locale, getPattern,
name: name,
currencySymbol: currencySymbol,
decimalDigits: decimalDigits,
lookupSimpleCurrencySymbol: lookupSimpleCurrencySymbol,
isForCurrency: isForCurrency) {
significantDigits = 3;
turnOffGrouping();
/// Map from magnitude to formatting pattern for that magnitude.
///
/// The magnitude is the exponent when using the normalized scientific
/// notation (so numbers from 1000 to 9999 correspond to magnitude 3).
///
/// These patterns are taken from the appropriate CompactNumberSymbols
/// instance's COMPACT_DECIMAL_SHORT_PATTERN, COMPACT_DECIMAL_LONG_PATTERN,
/// or COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN members.
Map<int, String> _patterns;
switch (formatType) {
case _CompactFormatType.COMPACT_DECIMAL_SHORT_PATTERN:
_patterns = _compactSymbols.COMPACT_DECIMAL_SHORT_PATTERN;
break;
// TODO(alanknight): Long formats may have different forms for different
// plural cases (e.g. million/millions).
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 ArgumentError.notNull('formatType');
}
_patterns.forEach((int exponent, String pattern) {
if (pattern.contains(';')) {
var patterns = pattern.split(';');
_styles.add(_CompactStyleWithNegative(
_createStyle(patterns.first, exponent),
_createStyle(patterns.last, exponent)));
} else {
_styles.add(_createStyle(pattern, exponent));
}
});
// 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(_CompactStyle());
}
final _regex = RegExp('([^0]*)(0+)(.*)');
final _justZeros = RegExp(r'^0*$');
/// Does pattern have any additional characters or is it just zeros.
bool _hasNonZeroContent(String pattern) => !_justZeros.hasMatch(pattern);
/// Creates a [_CompactStyle] instance for pattern with [normalizedExponent].
_CompactStyle _createStyle(String pattern, int normalizedExponent) {
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 (_hasNonZeroContent(pattern)) {
divisor = pow(10, normalizedExponent - integerDigits + 1);
}
return _CompactStyle(
pattern: pattern,
normalizedExponent: normalizedExponent,
expectedDigits: integerDigits,
prefix: prefix,
suffix: suffix,
divisor: divisor);
}
/// 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);
final divisor = _style.printsAsIs ? 1 : _style.divisor;
final 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 (_isForCurrency && !_style.isFallback) {
formatted = formatted.replaceFirst(currencySymbol, '').trim();
prefix = prefix.replaceFirst('\u00a4', currencySymbol);
suffix = suffix.replaceFirst('\u00a4', currencySymbol);
}
final 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) {
final 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);
}
}
/// Defines minimumFractionDigits based on current style being formatted.
@override
int get minimumFractionDigits {
if (!_isForCurrency ||
!significantDigitsInUse ||
_style == null ||
_style.isFallback) {
return super.minimumFractionDigits;
} else {
return 0;
}
}
/// 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 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.styleForSign(number);
}
}
throw FormatException(
'No compact style found for number. This should not happen', number);
}
Iterable<_CompactStyle> get _stylesForSearching =>
_styles.reversed.expand((x) => x.allStyles);
num parse(String text) {
for (var style in _stylesForSearching) {
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 FormatException(
"Cannot parse compact number in locale '$locale'", text);
}
/// Returns text parsed into a number if possible, else returns null.
num _tryParsing(String text) {
try {
return super.parse(text);
} on FormatException {
return null;
}
}
/// The [CompactNumberSymbols] instance that corresponds to the [_locale] this
/// [NumberFormat] instance was configured for.
CompactNumberSymbols get _compactSymbols => compactNumberSymbols[_locale];
}