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 /// Tests for compact format numbers, e.g. 1.2M rather than 1,200,000 |
| 6 import 'dart:math'; |
| 7 import 'package:test/test.dart'; |
| 8 import 'package:intl/intl.dart'; |
| 9 import 'package:fixnum/fixnum.dart'; |
| 10 import 'compact_number_test_data.dart' as testdata; |
| 11 |
| 12 /// A place to put a case that's causing a problem and have it run first when |
| 13 /// debugging |
| 14 var interestingCases = { |
| 15 // "mn" : [["4321", "4.32M", "whatever"]] |
| 16 }; |
| 17 |
| 18 main() { |
| 19 interestingCases.forEach(validate); |
| 20 testdata.compactNumberTestData.forEach(validate); |
| 21 |
| 22 // ICU doesn't support compact currencies yet, so we don't have a way to |
| 23 // generate automatic data for comparison. Hard-coded a couple of cases as a |
| 24 // smoke test. JPY is a useful test because it has no decimalDigits and |
| 25 // different grouping than USD, as well as a different currency symbol and |
| 26 // suffixes. |
| 27 testCurrency("ja", 1.2345, "¥1", "¥1"); |
| 28 testCurrency("ja", 1, "¥1", "¥1"); |
| 29 testCurrency("ja", 12, "¥12", "¥10"); |
| 30 testCurrency("ja", 123, "¥123", "¥100"); |
| 31 testCurrency("ja", 1234, "¥1230", "¥1000"); |
| 32 testCurrency("ja", 12345, "¥1.23\u4E07", "¥1\u4E07"); |
| 33 testCurrency("ja", 123456, "¥12.3\u4E07", "¥10\u4E07"); |
| 34 testCurrency("ja", 1234567, "¥123\u4e07", "¥100\u4e07"); |
| 35 testCurrency("ja", 12345678, "¥1230\u4e07", "¥1000\u4e07"); |
| 36 testCurrency("ja", 123456789, "¥1.23\u5104", "¥1\u5104"); |
| 37 |
| 38 testCurrency("ja", 0.9876, "¥1", "¥1"); |
| 39 testCurrency("ja", 9, "¥9", "¥9"); |
| 40 testCurrency("ja", 98, "¥98", "¥100"); |
| 41 testCurrency("ja", 987, "¥987", "¥1000"); |
| 42 testCurrency("ja", 9876, "¥9880", "¥1\u4E07"); |
| 43 testCurrency("ja", 98765, "¥9.88\u4E07", "¥10\u4E07"); |
| 44 testCurrency("ja", 987656, "¥98.8\u4E07", "¥100\u4E07"); |
| 45 testCurrency("ja", 9876567, "¥988\u4e07", "¥1000\u4e07"); |
| 46 testCurrency("ja", 98765678, "¥9880\u4e07", "¥1\u5104"); |
| 47 testCurrency("ja", 987656789, "¥9.88\u5104", "¥10\u5104"); |
| 48 |
| 49 testCurrency("en_US", 1.2345, r"$1.23", r"$1"); |
| 50 testCurrency("en_US", 1, r"$1.00", r"$1"); |
| 51 testCurrency("en_US", 12, r"$12.00", r"$10"); |
| 52 testCurrency("en_US", 12.3, r"$12.30", r"$10"); |
| 53 testCurrency("en_US", 123, r"$123", r"$100"); |
| 54 testCurrency("en_US", 1234, r"$1.23K", r"$1K"); |
| 55 testCurrency("en_US", 12345, r"$12.3K", r"$10K"); |
| 56 testCurrency("en_US", 123456, r"$123K", r"$100K"); |
| 57 testCurrency("en_US", 1234567, r"$1.23M", r"$1M"); |
| 58 |
| 59 // Check for order of currency symbol when currency is a suffix. |
| 60 testCurrency("ru", 4420, "4,42\u00A0тыс.\u00A0руб.", "4\u00A0тыс.\u00A0руб."); |
| 61 |
| 62 // Locales which don't have a suffix for thousands. |
| 63 testCurrency("it", 442, "442\u00A0€", "400\u00A0€"); |
| 64 testCurrency("it", 4420, "4420\u00A0\$", "4000\u00A0\$", currency: 'CAD'); |
| 65 testCurrency("it", 4420000, "4,42\u00A0Mio\u00A0\$", "4\u00A0Mio\u00A0\$", |
| 66 currency: 'USD'); |
| 67 |
| 68 test("Explicit non-default symbol with compactCurrency", () { |
| 69 var format = new NumberFormat.compactCurrency(locale: "ja", symbol: "()"); |
| 70 var result = format.format(98765); |
| 71 expect(result, "()9.88\u4e07"); |
| 72 }); |
| 73 } |
| 74 |
| 75 testCurrency(String locale, num number, String expected, String expectedShort, |
| 76 {String currency}) { |
| 77 test("Compact simple currency for $locale, $number", () { |
| 78 var format = |
| 79 new NumberFormat.compactSimpleCurrency(locale: locale, name: currency); |
| 80 var result = format.format(number); |
| 81 expect(result, expected); |
| 82 var shortFormat = |
| 83 new NumberFormat.compactSimpleCurrency(locale: locale, name: currency); |
| 84 shortFormat.significantDigits = 1; |
| 85 var shortResult = shortFormat.format(number); |
| 86 expect(shortResult, expectedShort); |
| 87 }); |
| 88 test("Compact currency for $locale, $number", () { |
| 89 var symbols = { |
| 90 "ja": "¥", |
| 91 "en_US": r"$", |
| 92 "ru": "руб.", |
| 93 "it": "€", |
| 94 "CAD": r"$", |
| 95 "USD": r"$" |
| 96 }; |
| 97 var symbol = symbols[currency] ?? symbols[locale]; |
| 98 var format = new NumberFormat.compactCurrency( |
| 99 locale: locale, name: currency, symbol: symbol); |
| 100 var result = format.format(number); |
| 101 expect(result, expected); |
| 102 var shortFormat = new NumberFormat.compactCurrency( |
| 103 locale: locale, name: currency, symbol: symbol); |
| 104 shortFormat.significantDigits = 1; |
| 105 var shortResult = shortFormat.format(number); |
| 106 expect(shortResult, expectedShort); |
| 107 }); |
| 108 } |
| 109 |
| 110 // TODO(alanknight): Don't just skip the whole locale if there's one problem |
| 111 // case. |
| 112 // TODO(alanknight): Fix the problems, or at least figure out precisely where |
| 113 // the differences are. |
| 114 var problemLocalesShort = [ |
| 115 "am", // AM Suffixes differ, not sure why. |
| 116 "ca", // For CA, CLDR rules are different. Jumps from 0000 to 00 prefix, so |
| 117 // 11 digits prints as 11900. |
| 118 "es_419", // Some odd formatting rules for these which seem to be different |
| 119 // from CLDR. wants e.g. '160000000000k' Actual: '160 B' |
| 120 "es_ES", // The reverse of es_419 for a few cases. We're printing a longer |
| 121 // form. |
| 122 "es_US", // Like es_419 but not as many of them. e.g. Expected: '87700k' |
| 123 // Actual: '87.7 M' |
| 124 "es_MX", // like es_419 |
| 125 "es", |
| 126 "fa", |
| 127 "fr_CA", // Several where PyICU isn't compacting. Expected: '988000000' |
| 128 // Actual: '988 M'. |
| 129 "gsw", // Suffixes disagree |
| 130 "in", // IN not compacting 54321, looks similar to tr. |
| 131 "id", // ID not compacting 54321, looks similar to tr. |
| 132 "ka", // K Slight difference in the suffix |
| 133 "kk", "mn", // We're picking the wrong pattern for 654321. |
| 134 "lo", "mk", "my", |
| 135 "pt_PT", // Seems to differ in appending mil or not after thousands. pt_BR |
| 136 // does it. |
| 137 "th", // TH Expected abbreviations as '1.09 พ.ล.' rather than '1.09 พ' |
| 138 "tr", // TR Doesn't have a 0B format, goes directly to 00B, as a result 54321 |
| 139 // just prints as 54321 |
| 140 "ur", // UR Fails one with Expected: '15 ٹریلین' Actual: '150 کھرب' |
| 141 ]; |
| 142 |
| 143 /// Locales that have problems in the long format. |
| 144 /// |
| 145 /// These are mostly minor differences in the characters, and many I can't read, |
| 146 /// but I'm suspicious many of them are essentially the difference between |
| 147 /// million and millions, which we don't distinguish. That's definitely the case |
| 148 /// with e.g. DE, but our data definitely has Millionen throughout. |
| 149 /// |
| 150 //TODO(alanknight): Narrow these down to particular numbers. Often it's just |
| 151 // 999999. |
| 152 var problemLocalesLong = [ |
| 153 "ar", |
| 154 "be", "bg", "bs", |
| 155 "ca", "cs", "da", "de", "de_AT", "de_CH", "el", "es", "es_419", "es_ES", |
| 156 "es_MX", "es_US", "et", "fi", |
| 157 "fil", // FIL is different, seems like a genuine difference in suffixes |
| 158 "fr", "fr_CA", "ga", "gl", |
| 159 "gsw", // GSW seems like we have long forms and pyICU doesn't |
| 160 "hr", "is", "it", "lo", // LO seems to be picking up a different pattern. |
| 161 "lt", "lv", "mk", |
| 162 "my", // Seems to come out in the reverse order |
| 163 "nb", "ne", "no", "no_NO", "pl", |
| 164 "pt", // PT has some issues with scale as well, but I think it's differences |
| 165 // in the patterns. |
| 166 "pt_BR", "pt_PT", "ro", "ru", "sk", "sl", "sr", "sr_Latn", "sv", "te", "tl", |
| 167 "ur", |
| 168 "uk", |
| 169 ]; |
| 170 |
| 171 void validate(String locale, List<List<String>> expected) { |
| 172 validateShort(locale, expected); |
| 173 validateLong(locale, expected); |
| 174 } |
| 175 |
| 176 /// Check each bit of test data against the short compact format, both |
| 177 /// formatting and parsing. |
| 178 void validateShort(String locale, List<List<String>> expected) { |
| 179 if (problemLocalesShort.contains(locale)) { |
| 180 print("Skipping problem locale '$locale' for SHORT compact number tests"); |
| 181 return; |
| 182 } |
| 183 var shortFormat = new NumberFormat.compact(locale: locale); |
| 184 for (var data in expected) { |
| 185 var number = num.parse(data.first); |
| 186 test("Validate $locale SHORT for ${data.first}", () { |
| 187 validateNumber(number, shortFormat, data[1]); |
| 188 }); |
| 189 var int64Number = new Int64(number); |
| 190 test("Validate Int64 SHORT on $locale for ${data.first}", () { |
| 191 validateNumber(int64Number, shortFormat, data[1]); |
| 192 }); |
| 193 // TODO(alanknight): Make this work for MicroMoney |
| 194 } |
| 195 } |
| 196 |
| 197 void validateLong(String locale, List<List<String>> expected) { |
| 198 if (problemLocalesLong.contains(locale)) { |
| 199 print("Skipping problem locale '$locale' for LONG compact number tests"); |
| 200 return; |
| 201 } |
| 202 var longFormat = new NumberFormat.compactLong(locale: locale); |
| 203 for (var data in expected) { |
| 204 var number = num.parse(data.first); |
| 205 test("Validate $locale LONG for ${data.first}", () { |
| 206 validateNumber(number, longFormat, data[2]); |
| 207 }); |
| 208 } |
| 209 } |
| 210 |
| 211 void validateNumber(number, NumberFormat format, String expected) { |
| 212 var formatted = format.format(number); |
| 213 var ok = closeEnough(formatted, expected); |
| 214 if (!ok) { |
| 215 expect( |
| 216 "$formatted ${formatted.codeUnits}", "$expected ${expected.codeUnits}"); |
| 217 } |
| 218 var parsed = format.parse(formatted); |
| 219 var rounded = roundForPrinting(number, format); |
| 220 expect((parsed - rounded) / rounded < 0.001, isTrue); |
| 221 } |
| 222 |
| 223 /// Duplicate a bit of the logic in formatting, where if we have a |
| 224 /// number that will round to print differently depending on the number |
| 225 /// of significant digits, we need to check that as well, e.g. |
| 226 /// 999999 may print as 1M. |
| 227 roundForPrinting(number, NumberFormat format) { |
| 228 var originalLength = NumberFormat.numberOfIntegerDigits(number); |
| 229 var additionalDigits = originalLength - format.significantDigits; |
| 230 if (additionalDigits > 0) { |
| 231 var divisor = pow(10, additionalDigits); |
| 232 // If we have an Int64, value speed over precision and make it double. |
| 233 var rounded = (number.toDouble() / divisor).round() * divisor; |
| 234 return rounded; |
| 235 } |
| 236 return number.toDouble(); |
| 237 } |
| 238 |
| 239 final _nbsp = 0xa0; |
| 240 final _nbspString = new String.fromCharCode(_nbsp); |
| 241 |
| 242 /// Return true if the strings are close enough to what we |
| 243 /// expected to consider a pass. |
| 244 /// |
| 245 /// In particular, there seem to be minor differences between what PyICU is |
| 246 /// currently producing and the CLDR data. So if the strings differ only in the |
| 247 /// presence or absence of a period at the end or of a space between the number |
| 248 /// and the suffix, consider it close enough and return true. |
| 249 bool closeEnough(String result, String reference) { |
| 250 var expected = reference.replaceAll(' ', _nbspString); |
| 251 if (result == expected) { |
| 252 return true; |
| 253 } |
| 254 if ('$result.' == expected) { |
| 255 return true; |
| 256 } |
| 257 if (result == '$expected.') { |
| 258 return true; |
| 259 } |
| 260 if (_oneSpaceOnlyDifference(result, expected)) { |
| 261 return true; |
| 262 } |
| 263 return false; |
| 264 } |
| 265 |
| 266 /// Do the two strings differ only by a single space being |
| 267 /// omitted in one of them. |
| 268 /// |
| 269 /// We assume non-breaking spaces because we |
| 270 /// know that's what the Intl data uses. We already know the strings aren't |
| 271 /// equal because that's checked first in the only caller. |
| 272 bool _oneSpaceOnlyDifference(String result, String expected) { |
| 273 var resultWithoutSpaces = |
| 274 new String.fromCharCodes(result.codeUnits.where((x) => x != _nbsp)); |
| 275 var expectedWithoutSpaces = |
| 276 new String.fromCharCodes(expected.codeUnits.where((x) => x != _nbsp)); |
| 277 var resultDifference = result.length - resultWithoutSpaces.length; |
| 278 var expectedDifference = expected.length - expectedWithoutSpaces.length; |
| 279 return (resultWithoutSpaces == expectedWithoutSpaces && |
| 280 resultDifference <= 1 && |
| 281 expectedDifference <= 1); |
| 282 } |
OLD | NEW |