blob: 708cb792d5870382ee2170d9ef353eab821f006c [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.
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 [number].
_CompactStyle styleForNumber(number);
/// 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 plural forms.
class _CompactStyleWithPlurals extends _CompactStyleBase {
int exponent;
Map<String, _CompactStyleBase> styles;
plural_rules.PluralCase Function()? _plural;
late _CompactStyleBase _defaultStyle;
_CompactStyleWithPlurals(this.styles, this.exponent, String? locale) {
_plural = plural_rules.pluralRules[locale];
_defaultStyle = styles['other']!;
}
@override
Iterable<_CompactStyle> get allStyles =>
styles.values.expand((x) => x.allStyles);
@override
int get divisor => _defaultStyle.divisor;
@override
_CompactStyle styleForNumber(final number) {
var value = number.abs();
if (_plural == null || value < 1) {
return _defaultStyle.styleForNumber(number);
}
var displayed = _CompactNumberFormat._divide(value, _defaultStyle.divisor);
// 3 significant digits.
if (displayed >= 100) {
displayed = displayed.round();
} else if (displayed >= 10) {
displayed = (displayed * 10).round() / 10;
} else {
// Note: >= 1.
displayed = (displayed * 100).round() / 100;
}
var afterDecimal = (displayed * 100).round() % 100; // At most 2 digits.
if (afterDecimal == 0) {
// This is displayed as integer: round.
displayed = displayed.round();
} else {
// Plural rules deal poorly with double: 2.01 * 100 = 200.999...
// So, add a little value: invisible, no effect on rounding.
displayed += 0.0001;
}
var precision = afterDecimal == 0 ? 0 : (afterDecimal % 10 == 0 ? 1 : 2);
plural_rules.startRuleEvaluation(displayed, precision);
var pluralCase = _plural!();
var style = _defaultStyle;
switch (pluralCase) {
case plural_rules.PluralCase.ZERO:
style = styles['zero'] ?? _defaultStyle;
break;
case plural_rules.PluralCase.ONE:
style = styles['one'] ?? _defaultStyle;
break;
case plural_rules.PluralCase.TWO:
style = styles['two'] ?? styles['few'] ?? _defaultStyle;
break;
case plural_rules.PluralCase.FEW:
style = styles['few'] ?? _defaultStyle;
break;
case plural_rules.PluralCase.MANY:
style = styles['many'] ?? _defaultStyle;
break;
default:
// Keep _defaultStyle;
}
return style.styleForNumber(number);
}
}
/// 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 styleForNumber(number) =>
number < 0 ? negativeStyle : positiveStyle;
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', divisor: 1000,
/// prefix: '', suffix: 'K');
class _CompactStyle extends _CompactStyleBase {
_CompactStyle(
{this.pattern,
this.divisor = 1,
this.positivePrefix = '',
this.negativePrefix = '',
this.positiveSuffix = '',
this.negativeSuffix = ''});
/// The pattern on which this is based.
///
/// We don't actually need this, but it makes debugging easier.
String? pattern;
/// 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;
// Prefixes / suffixes.
String positivePrefix;
String negativePrefix;
String positiveSuffix;
String negativeSuffix;
/// 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';
_CompactStyle styleForNumber(number) => this;
List<_CompactStyle> get allStyles => [this];
static final _regex = RegExp('([^0]*)(0+)(.*)');
static final _justZeros = RegExp(r'^0*$');
/// Does pattern have any additional characters or is it just zeros.
static bool _hasNonZeroContent(String pattern) =>
!_justZeros.hasMatch(pattern);
/// Creates a [_CompactStyle] instance for pattern with [normalizedExponent].
static _CompactStyle createStyle(
NumberSymbols symbols, String pattern, int normalizedExponent,
{bool isSigned = false, bool explicitSign = false}) {
var prefix = '';
var suffix = '';
var divisor = 1;
var match = _regex.firstMatch(pattern);
if (match != null) {
prefix = match.group(1)!;
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.
if (_hasNonZeroContent(pattern)) {
var integerDigits = match.group(2)!.length;
divisor = pow(10, normalizedExponent - integerDigits + 1) as int;
}
}
final positivePrefix =
(explicitSign && !isSigned) ? '${symbols.PLUS_SIGN}$prefix' : prefix;
final negativePrefix =
(!isSigned) ? '${symbols.MINUS_SIGN}$prefix' : prefix;
final positiveSuffix = suffix;
final negativeSuffix = suffix;
return _CompactStyle(
pattern: pattern,
positivePrefix: positivePrefix,
negativePrefix: negativePrefix,
positiveSuffix: positiveSuffix,
negativeSuffix: negativeSuffix,
divisor: divisor);
}
}
/// 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;
// Map exponent => style.
final Map<int, _CompactStyleBase> _styles;
// Whether positive sign should be explicitly printed.
final bool _explicitSign;
factory _CompactNumberFormat(
{String? locale,
_CompactFormatType? formatType,
String? name,
String? currencySymbol,
String? Function(NumberSymbols) getPattern = _forDecimal,
int? decimalDigits,
bool explicitSign = false,
bool lookupSimpleCurrencySymbol = false,
bool isForCurrency = false}) {
// Initialization copied from `NumberFormat` constructor.
// TODO(davidmorgan): deduplicate.
locale = helpers.verifiedLocale(locale, NumberFormat.localeExists, null)!;
var symbols = numberFormatSymbols[locale] as NumberSymbols;
var localeZero = symbols.ZERO_DIGIT.codeUnitAt(0);
var zeroOffset = localeZero - constants.asciiZeroCodeUnit;
name ??= symbols.DEF_CURRENCY_CODE;
if (currencySymbol == null && lookupSimpleCurrencySymbol) {
currencySymbol = constants.simpleCurrencySymbols[name];
}
currencySymbol ??= name;
var pattern = getPattern(symbols);
// CompactNumberFormat initialization.
/// 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, Map<String, String>> patterns;
var compactSymbols = compactNumberSymbols[locale]!;
var styles = <int, _CompactStyleBase>{};
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 ArgumentError.notNull('formatType');
}
patterns.forEach((int exponent, Map<String, String> patterns) {
_CompactStyleBase style;
if (patterns.keys.length == 1 && patterns.keys.single == "other") {
// No plural.
var pattern = patterns.values.single;
style = _styleFromPattern(pattern, exponent, explicitSign, symbols);
} else {
style = _CompactStyleWithPlurals(
patterns.map((key, value) => MapEntry(key,
_styleFromPattern(value, exponent, explicitSign, symbols))),
exponent,
locale);
}
styles[exponent] = style;
});
return _CompactNumberFormat._(
name,
currencySymbol,
isForCurrency,
locale,
localeZero,
pattern,
symbols,
zeroOffset,
NumberFormatParser.parse(symbols, pattern, isForCurrency,
currencySymbol, name, decimalDigits),
styles,
explicitSign);
}
static _CompactStyleBase _styleFromPattern(
String pattern, int exponent, bool explicitSign, NumberSymbols symbols) {
if (pattern.contains(';')) {
var patterns = pattern.split(';');
var positivePattern = patterns.first;
var negativePattern = patterns.last;
if (explicitSign &&
!positivePattern.contains(symbols.PLUS_SIGN) &&
negativePattern.contains(symbols.MINUS_SIGN) &&
positivePattern ==
negativePattern.replaceAll(symbols.MINUS_SIGN, '')) {
// Re-use the negative pattern, with plus sign.
positivePattern =
negativePattern.replaceAll(symbols.MINUS_SIGN, symbols.PLUS_SIGN);
}
return _CompactStyleWithNegative(
_CompactStyle.createStyle(symbols, positivePattern, exponent,
isSigned: positivePattern.contains(symbols.PLUS_SIGN)),
_CompactStyle.createStyle(symbols, negativePattern, exponent,
isSigned: true));
} else {
return _CompactStyle.createStyle(symbols, pattern, exponent,
explicitSign: explicitSign);
}
}
_CompactNumberFormat._(
String currencyName,
String currencySymbol,
bool isForCurrency,
String locale,
int localeZero,
String? pattern,
NumberSymbols symbols,
int zeroOffset,
NumberFormatParseResult result,
// Fields introduced in this class.
this._styles,
this._explicitSign)
: super._(currencyName, currencySymbol, isForCurrency, locale, localeZero,
pattern, symbols, zeroOffset, result) {
significantDigits = 3;
turnOffGrouping();
}
/// The style in which we will format a particular number.
///
/// This is a temporary variable that is only valid within a call to format
/// and parse.
_CompactStyle? _style;
// We delegate prefixes to current _style.
String get positivePrefix =>
_style!.isFallback ? super.positivePrefix : _style!.positivePrefix;
String get negativePrefix =>
_style!.isFallback ? super.negativePrefix : _style!.negativePrefix;
String get positiveSuffix =>
_style!.isFallback ? super.positiveSuffix : _style!.positiveSuffix;
String get negativeSuffix =>
_style!.isFallback ? super.negativeSuffix : _style!.negativeSuffix;
String format(number) {
var style = _styleFor(number);
_style = style;
final divisor = style.isFallback ? 1 : style.divisor;
final numberToFormat = _divide(number, divisor);
var formatted = super.format(numberToFormat);
if (_explicitSign &&
style.isFallback &&
number >= 0 &&
!formatted.contains(symbols.PLUS_SIGN)) {
formatted = '${symbols.PLUS_SIGN}$formatted';
}
if (_isForCurrency && !style.isFallback) {
formatted = formatted.replaceFirst('\u00a4', currencySymbol);
}
_style = null;
return formatted;
}
/// 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.
static 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);
}
var expectedExponent = digitLength - 1;
_CompactStyleBase? style;
for (var entries in _styles.entries) {
if (entries.key > expectedExponent) {
break;
}
style = entries.value;
}
return style?.styleForNumber(number) ?? _defaultCompactStyle;
}
Iterable<_CompactStyle> get _stylesForSearching =>
_styles.values.expand((x) => x.allStyles);
String _normalize(String input) {
return input
.replaceAll('\u200e', '') // LEFT-TO-RIGHT MARK.
.replaceAll('\u200f', '') // RIGHT-TO-LEFT MARK.
.replaceAll('\u0020', '') // SPACE.
.replaceAll('\u00a0', '') // NO-BREAK SPACE.
.replaceAll('\u202f', '') // NARROW NO-BREAK SPACE.
.replaceAll('\u2212', '-'); // MINUS SIGN.
}
num parse(final String inputText) {
for (var style in [_defaultCompactStyle, ..._stylesForSearching]) {
_style = style;
var text = _normalize(inputText);
var negative = false;
var negativePrefix = _normalize(style.negativePrefix);
var negativeSuffix = _normalize(style.negativeSuffix);
var positivePrefix = _normalize(style.positivePrefix);
var positiveSuffix = _normalize(style.positiveSuffix);
if (!style.isFallback) {
if (text.startsWith(negativePrefix) && text.endsWith(negativeSuffix)) {
text = text.substring(
negativePrefix.length, text.length - negativeSuffix.length);
negative = true;
} else if (text.startsWith(positivePrefix) &&
text.endsWith(positiveSuffix)) {
text = text.substring(
positivePrefix.length, text.length - positiveSuffix.length);
} else {
continue;
}
}
var number = _tryParsing(text);
if (number == null && _zeroOffset != 0) {
// Locale has non-roman numerals.
// Try simple number parse, in case input contains roman numerals.
number = num.tryParse(text);
}
if (number != null) {
_style = null;
return number * style.divisor * (negative ? -1 : 1);
}
}
_style = null;
throw FormatException(
"Cannot parse compact number in locale '$locale'", inputText);
}
/// Returns text parsed into a number if possible, else returns null.
num? _tryParsing(String text) {
try {
return super.parse(text);
} on FormatException {
return null;
}
}
}
final _defaultCompactStyle = _CompactStyle();