OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 part of intl; |
| 6 |
| 7 /// Represents a compact format for a particular base |
| 8 /// |
| 9 /// For example, 10k can be used to represent 10,000. Corresponds to one of the |
| 10 /// patterns in COMPACT_DECIMAL_SHORT_FORMAT. So, for example, in en_US we have |
| 11 /// the pattern |
| 12 /// |
| 13 /// 4: '0K' |
| 14 /// which matches |
| 15 /// |
| 16 /// new _CompactStyle(pattern: '0K', requiredDigits: 4, divisor: 1000, |
| 17 /// expectedDigits: 1, prefix: '', suffix: 'K'); |
| 18 /// |
| 19 /// where expectedDigits is the number of zeros. |
| 20 class _CompactStyle { |
| 21 _CompactStyle( |
| 22 {this.pattern, |
| 23 this.requiredDigits: 0, |
| 24 this.divisor: 1, |
| 25 this.expectedDigits: 1, |
| 26 this.prefix: '', |
| 27 this.suffix: ''}); |
| 28 |
| 29 /// The pattern on which this is based. |
| 30 /// |
| 31 /// We don't actually need this, but it makes debugging easier. |
| 32 String pattern; |
| 33 |
| 34 /// The length for which the format applies. |
| 35 /// |
| 36 /// So if this is 3, we expect it to apply to numbers from 100 up. Typically |
| 37 /// it would be from 100 to 1000, but that depends if there's a style for 4 or |
| 38 /// not. This is the CLDR index of the pattern, and usually determines the |
| 39 /// divisor, but if the pattern is just a 0 with no prefix or suffix then we |
| 40 /// don't divide at all. |
| 41 int requiredDigits; |
| 42 |
| 43 /// What should we divide the number by in order to print. Normally is either |
| 44 /// 10^requiredDigits or 1 if we shouldn't divide at all. |
| 45 int divisor; |
| 46 |
| 47 /// How many integer digits do we expect to print - the number of zeros in the |
| 48 /// CLDR pattern. |
| 49 int expectedDigits; |
| 50 |
| 51 /// Text we put in front of the number part. |
| 52 String prefix; |
| 53 |
| 54 /// Text we put after the number part. |
| 55 String suffix; |
| 56 |
| 57 /// How many total digits do we expect in the number. |
| 58 /// |
| 59 /// If the pattern is |
| 60 /// |
| 61 /// 4: "00K", |
| 62 /// |
| 63 /// then this is 5, meaning we expect this to be a 5-digit (or more) |
| 64 /// number. We will scale by 1000 and expect 2 integer digits remaining, so we |
| 65 /// get something like '12K'. This is used to find the closest pattern for a |
| 66 /// number. |
| 67 get totalDigits => requiredDigits + expectedDigits - 1; |
| 68 |
| 69 /// Return true if this is the fallback compact pattern, printing the number |
| 70 /// un-compacted. e.g. 1200 might print as "1.2K", but 12 just prints as "12". |
| 71 /// |
| 72 /// For currencies, with the fallback pattern we use the super implementation |
| 73 /// so that we will respect things like the default number of decimal digits |
| 74 /// for a particular currency (e.g. two for USD, zero for JPY) |
| 75 bool get isFallback => pattern == null || pattern == '0'; |
| 76 |
| 77 /// Should we print the number as-is, without dividing. |
| 78 /// |
| 79 /// This happens if the pattern has no abbreviation for scaling (e.g. K, M). |
| 80 /// So either the pattern is empty or it is of a form like '0 $'. This is a |
| 81 /// workaround for locales like "it", which include patterns with no suffix |
| 82 /// for numbers >= 1000 but < 1,000,000. |
| 83 bool get printsAsIs => |
| 84 isFallback || |
| 85 pattern.replaceAll(new RegExp('[0\u00a0\u00a4]'), '').isEmpty; |
| 86 } |
| 87 |
| 88 enum _CompactFormatType { |
| 89 COMPACT_DECIMAL_SHORT_PATTERN, |
| 90 COMPACT_DECIMAL_LONG_PATTERN, |
| 91 COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN |
| 92 } |
| 93 |
| 94 class _CompactNumberFormat extends NumberFormat { |
| 95 /// A default, using the decimal pattern, for the [getPattern] constructor par
ameter. |
| 96 static String _forDecimal(NumberSymbols symbols) => symbols.DECIMAL_PATTERN; |
| 97 |
| 98 // Will be either the COMPACT_DECIMAL_SHORT_PATTERN, |
| 99 // COMPACT_DECIMAL_LONG_PATTERN, or COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN |
| 100 Map<int, String> _patterns; |
| 101 |
| 102 List<_CompactStyle> _styles = []; |
| 103 |
| 104 _CompactNumberFormat( |
| 105 {String locale, |
| 106 _CompactFormatType formatType, |
| 107 String name, |
| 108 String currencySymbol, |
| 109 String getPattern(NumberSymbols symbols): _forDecimal, |
| 110 String computeCurrencySymbol(NumberFormat), |
| 111 int decimalDigits, |
| 112 bool isForCurrency: false}) |
| 113 : super._forPattern(locale, getPattern, |
| 114 name: name, |
| 115 currencySymbol: currencySymbol, |
| 116 computeCurrencySymbol: computeCurrencySymbol, |
| 117 decimalDigits: decimalDigits, |
| 118 isForCurrency: isForCurrency) { |
| 119 significantDigits = 3; |
| 120 turnOffGrouping(); |
| 121 switch (formatType) { |
| 122 case _CompactFormatType.COMPACT_DECIMAL_SHORT_PATTERN: |
| 123 _patterns = compactSymbols.COMPACT_DECIMAL_SHORT_PATTERN; |
| 124 break; |
| 125 case _CompactFormatType.COMPACT_DECIMAL_LONG_PATTERN: |
| 126 _patterns = compactSymbols.COMPACT_DECIMAL_LONG_PATTERN ?? |
| 127 compactSymbols.COMPACT_DECIMAL_SHORT_PATTERN; |
| 128 break; |
| 129 case _CompactFormatType.COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN: |
| 130 _patterns = compactSymbols.COMPACT_DECIMAL_SHORT_CURRENCY_PATTERN; |
| 131 break; |
| 132 default: |
| 133 throw new ArgumentError.notNull("formatType"); |
| 134 } |
| 135 var regex = new RegExp('([^0]*)(0+)(.*)'); |
| 136 _patterns.forEach((int impliedDigits, String pattern) { |
| 137 var match = regex.firstMatch(pattern); |
| 138 var integerDigits = match.group(2).length; |
| 139 var prefix = match.group(1); |
| 140 var suffix = match.group(3); |
| 141 // If the pattern is just zeros, with no suffix, then we shouldn't divide |
| 142 // by the number of digits. e.g. for 'af', the pattern for 3 is '0', but |
| 143 // it doesn't mean that 4321 should print as 4. But if the pattern was |
| 144 // '0K', then it should print as '4K'. So we have to check if the pattern |
| 145 // has a suffix. This seems extremely hacky, but I don't know how else to |
| 146 // encode that. Check what other things are doing. |
| 147 var divisor = 1; |
| 148 if (pattern.replaceAll('0', '').isNotEmpty) { |
| 149 divisor = pow(10, impliedDigits - integerDigits + 1); |
| 150 } |
| 151 var style = new _CompactStyle( |
| 152 pattern: pattern, |
| 153 requiredDigits: impliedDigits, |
| 154 expectedDigits: integerDigits, |
| 155 prefix: prefix, |
| 156 suffix: suffix, |
| 157 divisor: divisor); |
| 158 _styles.add(style); |
| 159 }); |
| 160 // Reverse the styles so that we look through them from largest to smallest. |
| 161 _styles = _styles.reversed.toList(); |
| 162 // Add a fallback style that just prints the number. |
| 163 _styles.add(new _CompactStyle()); |
| 164 } |
| 165 |
| 166 /// The style in which we will format a particular number. |
| 167 /// |
| 168 /// This is a temporary variable that is only valid within a call to format. |
| 169 _CompactStyle _style; |
| 170 |
| 171 String format(number) { |
| 172 _style = _styleFor(number); |
| 173 var divisor = _style.printsAsIs ? 1 : _style.divisor; |
| 174 var numberToFormat = _divide(number, divisor); |
| 175 var formatted = super.format(numberToFormat); |
| 176 var prefix = _style.prefix; |
| 177 var suffix = _style.suffix; |
| 178 // If this is for a currency, then the super call will have put the currency |
| 179 // somewhere. We don't want it there, we want it where our style indicates, |
| 180 // so remove it and replace. This has the remote possibility of a false |
| 181 // positive, but it seems unlikely that e.g. USD would occur as a string in |
| 182 // a regular number. |
| 183 if (this._isForCurrency && !_style.isFallback) { |
| 184 formatted = formatted.replaceFirst(currencySymbol, '').trim(); |
| 185 prefix = prefix.replaceFirst('\u00a4', currencySymbol); |
| 186 suffix = suffix.replaceFirst('\u00a4', currencySymbol); |
| 187 } |
| 188 var withExtras = "${prefix}$formatted${suffix}"; |
| 189 _style = null; |
| 190 return withExtras; |
| 191 } |
| 192 |
| 193 /// How many digits after the decimal place should we display, given that |
| 194 /// there are [remainingSignificantDigits] left to show. |
| 195 int _fractionDigitsAfter(int remainingSignificantDigits) { |
| 196 var newFractionDigits = |
| 197 super._fractionDigitsAfter(remainingSignificantDigits); |
| 198 // For non-currencies, or for currencies if the numbers are large enough to |
| 199 // compact, always use the number of significant digits and ignore |
| 200 // decimalDigits. That is, $1.23K but also ¥12.3\u4E07, even though yen |
| 201 // don't normally print decimal places. |
| 202 if (!_isForCurrency || !_style.isFallback) return newFractionDigits; |
| 203 // If we are printing a currency and it's too small to compact, but |
| 204 // significant digits would have us only print some of the decimal digits, |
| 205 // use all of them. So $12.30, not $12.3 |
| 206 if (newFractionDigits > 0 && newFractionDigits < decimalDigits) { |
| 207 return decimalDigits; |
| 208 } else { |
| 209 return min(newFractionDigits, decimalDigits); |
| 210 } |
| 211 } |
| 212 |
| 213 /// Divide numbers that may not have a division operator (e.g. Int64). |
| 214 /// |
| 215 /// Only used for powers of 10, so we require an integer denominator. |
| 216 num _divide(numerator, int denominator) { |
| 217 if (numerator is num) { |
| 218 return numerator / denominator; |
| 219 } |
| 220 // If it doesn't fit in a JS int after division, we're not going to be able |
| 221 // to meaningfully print a compact representation for it. |
| 222 var divided = numerator ~/ denominator; |
| 223 var integerPart = divided.toInt(); |
| 224 if (divided != integerPart) { |
| 225 throw new FormatException( |
| 226 "Number too big to use with compact format", numerator); |
| 227 } |
| 228 var remainder = numerator.remainder(denominator).toInt(); |
| 229 var originalFraction = numerator - (numerator ~/ 1); |
| 230 var fraction = originalFraction == 0 ? 0 : originalFraction / denominator; |
| 231 return integerPart + (remainder / denominator) + fraction; |
| 232 } |
| 233 |
| 234 _CompactStyle _styleFor(number) { |
| 235 // We have to round the number based on the number of significant digits so |
| 236 // that we pick the right style based on the rounded form and format 999999 |
| 237 // as 1M rather than 1000K. |
| 238 var originalLength = NumberFormat.numberOfIntegerDigits(number); |
| 239 var additionalDigits = originalLength - significantDigits; |
| 240 var digitLength = originalLength; |
| 241 if (additionalDigits > 0) { |
| 242 var divisor = pow(10, additionalDigits); |
| 243 // If we have an Int64, value speed over precision and make it double. |
| 244 var rounded = (number.toDouble() / divisor).round() * divisor; |
| 245 digitLength = NumberFormat.numberOfIntegerDigits(rounded); |
| 246 } |
| 247 for (var style in _styles) { |
| 248 if (digitLength > style.totalDigits) { |
| 249 return style; |
| 250 } |
| 251 } |
| 252 throw new FormatException( |
| 253 "No compact style found for number. This should not happen", number); |
| 254 } |
| 255 |
| 256 num parse(String text) { |
| 257 for (var style in _styles.reversed) { |
| 258 if (text.startsWith(style.prefix) && text.endsWith(style.suffix)) { |
| 259 var numberText = text.substring( |
| 260 style.prefix.length, text.length - style.suffix.length); |
| 261 var number = _tryParsing(numberText); |
| 262 if (number != null) { |
| 263 return number * style.divisor; |
| 264 } |
| 265 } |
| 266 } |
| 267 throw new FormatException( |
| 268 "Cannot parse compact number in locale '$locale'", text); |
| 269 } |
| 270 |
| 271 num _tryParsing(String text) { |
| 272 try { |
| 273 return super.parse(text); |
| 274 } on FormatException { |
| 275 return null; |
| 276 } |
| 277 } |
| 278 |
| 279 CompactNumberSymbols get compactSymbols => compactNumberSymbols[_locale]; |
| 280 } |
OLD | NEW |