Dart intl: fix compact number formatting.
To be consistent with other intl, do not try to outsmart exponent-based templates in CLDR.
PiperOrigin-RevId: 426127334
diff --git a/lib/src/intl/compact_number_format.dart b/lib/src/intl/compact_number_format.dart
index 32c94ab..0d4d52d 100644
--- a/lib/src/intl/compact_number_format.dart
+++ b/lib/src/intl/compact_number_format.dart
@@ -13,18 +13,6 @@
/// 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;
@@ -43,7 +31,6 @@
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];
}
@@ -57,59 +44,27 @@
/// 4: '00K'
/// which matches
///
-/// _CompactStyle(pattern: '00K', normalizedExponent: 4, divisor: 1000,
-/// expectedDigits: 1, prefix: '', suffix: 'K');
-///
-/// where expectedDigits is the number of zeros.
+/// _CompactStyle(pattern: '00K', divisor: 1000,
+/// prefix: '', suffix: 'K');
class _CompactStyle extends _CompactStyleBase {
_CompactStyle(
- {this.pattern,
- this.normalizedExponent = 0,
- this.divisor = 1,
- this.expectedDigits = 1,
- this.prefix = '',
- this.suffix = ''});
+ {this.pattern, this.divisor = 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'.
///
@@ -118,15 +73,6 @@
/// 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];
@@ -155,12 +101,7 @@
divisor = pow(10, normalizedExponent - integerDigits + 1) as int;
}
return _CompactStyle(
- pattern: pattern,
- normalizedExponent: normalizedExponent,
- expectedDigits: integerDigits,
- prefix: prefix,
- suffix: suffix,
- divisor: divisor);
+ pattern: pattern, prefix: prefix, suffix: suffix, divisor: divisor);
}
}
@@ -175,7 +116,8 @@
/// A default, using the decimal pattern, for the `getPattern` constructor parameter.
static String _forDecimal(NumberSymbols symbols) => symbols.DECIMAL_PATTERN;
- final List<_CompactStyleBase> _styles;
+ // Map exponent => style.
+ final Map<int, _CompactStyleBase> _styles;
factory _CompactNumberFormat(
{String? locale,
@@ -213,7 +155,7 @@
var compactSymbols = compactNumberSymbols[locale]!;
- var styles = <_CompactStyleBase>[];
+ var styles = <int, _CompactStyleBase>{};
switch (formatType) {
case _CompactFormatType.COMPACT_DECIMAL_SHORT_PATTERN:
patterns = compactSymbols.COMPACT_DECIMAL_SHORT_PATTERN;
@@ -233,19 +175,14 @@
patterns.forEach((int exponent, String pattern) {
if (pattern.contains(';')) {
var patterns = pattern.split(';');
- styles.add(_CompactStyleWithNegative(
+ styles[exponent] = _CompactStyleWithNegative(
_CompactStyle.createStyle(patterns.first, exponent),
- _CompactStyle.createStyle(patterns.last, exponent)));
+ _CompactStyle.createStyle(patterns.last, exponent));
} else {
- styles.add(_CompactStyle.createStyle(pattern, exponent));
+ styles[exponent] = _CompactStyle.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());
-
return _CompactNumberFormat._(
name,
currencySymbol,
@@ -286,7 +223,7 @@
String format(number) {
var style = _styleFor(number);
_style = style;
- final divisor = style.printsAsIs ? 1 : style.divisor;
+ final divisor = style.isFallback ? 1 : style.divisor;
final numberToFormat = _divide(number, divisor);
var formatted = super.format(numberToFormat);
var prefix = style.prefix;
@@ -373,20 +310,23 @@
var rounded = (number.toDouble() / divisor).round() * divisor;
digitLength = NumberFormat.numberOfIntegerDigits(rounded);
}
- for (var style in _styles) {
- if (digitLength > style.totalDigits) {
- return style.styleForSign(number);
+
+ var expectedExponent = digitLength - 1;
+ _CompactStyleBase? style;
+ for (var entries in _styles.entries) {
+ if (entries.key > expectedExponent) {
+ break;
}
+ style = entries.value;
}
- throw FormatException(
- 'No compact style found for number. This should not happen', number);
+ return style?.styleForSign(number) ?? _defaultCompactStyle;
}
Iterable<_CompactStyle> get _stylesForSearching =>
- _styles.reversed.expand((x) => x.allStyles);
+ _styles.values.expand((x) => x.allStyles);
num parse(String text) {
- for (var style in _stylesForSearching) {
+ for (var style in [_defaultCompactStyle, ..._stylesForSearching]) {
if (text.startsWith(style.prefix) && text.endsWith(style.suffix)) {
var numberText = text.substring(
style.prefix.length, text.length - style.suffix.length);
@@ -409,3 +349,5 @@
}
}
}
+
+final _defaultCompactStyle = _CompactStyle();
diff --git a/test/number_format_compact_test.dart b/test/number_format_compact_test.dart
index 4b82be5..c08b31d 100644
--- a/test/number_format_compact_test.dart
+++ b/test/number_format_compact_test.dart
@@ -168,24 +168,8 @@
// TODO(alanknight): Don't just skip the whole locale if there's one problem
// case.
-// TODO(alanknight): Fix the problems, or at least figure out precisely where
-// the differences are.
-var _skipLocalsShort = {
- 'bn', // Bad prefix.
- 'ca', // For CA, CLDR rules are different. Jumps from 0000 to 00 prefix, so
- // 11 digits prints as 11900.
- 'es_419', // Some odd formatting rules for these which seem to be different
- // from CLDR. wants e.g. '160000000000k' Actual: '160 B'
- 'es_ES', // The reverse of es_419 for a few cases. We're printing a longer
- // form.
- 'es_US', // Like es_419 but not as many of them. e.g. Expected: '87700k'
- // Actual: '87.7 M'
- 'es_MX', // like es_419
- 'es',
- 'ka', // K Slight difference in the suffix
- 'kk', 'mn', // We're picking the wrong pattern for 654321.
- 'lo', 'mk', 'my',
- 'ur', // UR Fails one with Expected: '15 ٹریلین' Actual: '150 کھرب'
+var _skipLocalsShort = <String>{
+ // None ;o)
};
/// Locales that have problems in the long format.
@@ -197,7 +181,7 @@
///
//TODO(alanknight): Narrow these down to particular numbers. Often it's just
// 999999.
-var _skipLocalesLong = {
+var _skipLocalesLong = <String>{
'ar', 'ar_DZ', 'ar_EG',
'be', 'bg', 'bn', 'bs',
'ca', 'cs', 'da', 'de', 'de_AT', 'de_CH', 'el', 'es', 'es_419', 'es_ES',
@@ -208,10 +192,9 @@
'ga', 'gl',
'gsw', // GSW seems like we have long forms and pyICU doesn't
'hr', 'is', 'it', 'it_CH',
- 'km',
'lt', 'lv', 'mk',
'my', // Seems to come out in the reverse order
- 'nb', 'ne', 'no', 'no_NO', 'pl',
+ 'nb', 'no', 'no_NO', 'pl',
'pt', // PT has some issues with scale as well, but I think it's differences
// in the patterns.
'pt_BR', 'pt_PT', 'ro', 'ru',
@@ -265,7 +248,8 @@
}
var parsed = format.parse(formatted);
var rounded = _roundForPrinting(number, format);
- expect((parsed - rounded) / rounded < 0.001, isTrue);
+ expect((parsed - rounded) / rounded < 0.001, isTrue,
+ reason: 'Parsed: $parsed, rounded: $rounded');
}
/// Duplicate a bit of the logic in formatting, where if we have a