Index: packages/intl/test/number_format_compact_test.dart |
diff --git a/packages/intl/test/number_format_compact_test.dart b/packages/intl/test/number_format_compact_test.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..607dc20f2b9053c83ca7a11434d2b00f3fdf3704 |
--- /dev/null |
+++ b/packages/intl/test/number_format_compact_test.dart |
@@ -0,0 +1,282 @@ |
+/// 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. |
+ |
+/// Tests for compact format numbers, e.g. 1.2M rather than 1,200,000 |
+import 'dart:math'; |
+import 'package:test/test.dart'; |
+import 'package:intl/intl.dart'; |
+import 'package:fixnum/fixnum.dart'; |
+import 'compact_number_test_data.dart' as testdata; |
+ |
+/// A place to put a case that's causing a problem and have it run first when |
+/// debugging |
+var interestingCases = { |
+// "mn" : [["4321", "4.32M", "whatever"]] |
+}; |
+ |
+main() { |
+ interestingCases.forEach(validate); |
+ testdata.compactNumberTestData.forEach(validate); |
+ |
+ // ICU doesn't support compact currencies yet, so we don't have a way to |
+ // generate automatic data for comparison. Hard-coded a couple of cases as a |
+ // smoke test. JPY is a useful test because it has no decimalDigits and |
+ // different grouping than USD, as well as a different currency symbol and |
+ // suffixes. |
+ testCurrency("ja", 1.2345, "¥1", "¥1"); |
+ testCurrency("ja", 1, "¥1", "¥1"); |
+ testCurrency("ja", 12, "¥12", "¥10"); |
+ testCurrency("ja", 123, "¥123", "¥100"); |
+ testCurrency("ja", 1234, "¥1230", "¥1000"); |
+ testCurrency("ja", 12345, "¥1.23\u4E07", "¥1\u4E07"); |
+ testCurrency("ja", 123456, "¥12.3\u4E07", "¥10\u4E07"); |
+ testCurrency("ja", 1234567, "¥123\u4e07", "¥100\u4e07"); |
+ testCurrency("ja", 12345678, "¥1230\u4e07", "¥1000\u4e07"); |
+ testCurrency("ja", 123456789, "¥1.23\u5104", "¥1\u5104"); |
+ |
+ testCurrency("ja", 0.9876, "¥1", "¥1"); |
+ testCurrency("ja", 9, "¥9", "¥9"); |
+ testCurrency("ja", 98, "¥98", "¥100"); |
+ testCurrency("ja", 987, "¥987", "¥1000"); |
+ testCurrency("ja", 9876, "¥9880", "¥1\u4E07"); |
+ testCurrency("ja", 98765, "¥9.88\u4E07", "¥10\u4E07"); |
+ testCurrency("ja", 987656, "¥98.8\u4E07", "¥100\u4E07"); |
+ testCurrency("ja", 9876567, "¥988\u4e07", "¥1000\u4e07"); |
+ testCurrency("ja", 98765678, "¥9880\u4e07", "¥1\u5104"); |
+ testCurrency("ja", 987656789, "¥9.88\u5104", "¥10\u5104"); |
+ |
+ testCurrency("en_US", 1.2345, r"$1.23", r"$1"); |
+ testCurrency("en_US", 1, r"$1.00", r"$1"); |
+ testCurrency("en_US", 12, r"$12.00", r"$10"); |
+ testCurrency("en_US", 12.3, r"$12.30", r"$10"); |
+ testCurrency("en_US", 123, r"$123", r"$100"); |
+ testCurrency("en_US", 1234, r"$1.23K", r"$1K"); |
+ testCurrency("en_US", 12345, r"$12.3K", r"$10K"); |
+ testCurrency("en_US", 123456, r"$123K", r"$100K"); |
+ testCurrency("en_US", 1234567, r"$1.23M", r"$1M"); |
+ |
+ // Check for order of currency symbol when currency is a suffix. |
+ testCurrency("ru", 4420, "4,42\u00A0тыс.\u00A0руб.", "4\u00A0тыс.\u00A0руб."); |
+ |
+ // Locales which don't have a suffix for thousands. |
+ testCurrency("it", 442, "442\u00A0€", "400\u00A0€"); |
+ testCurrency("it", 4420, "4420\u00A0\$", "4000\u00A0\$", currency: 'CAD'); |
+ testCurrency("it", 4420000, "4,42\u00A0Mio\u00A0\$", "4\u00A0Mio\u00A0\$", |
+ currency: 'USD'); |
+ |
+ test("Explicit non-default symbol with compactCurrency", () { |
+ var format = new NumberFormat.compactCurrency(locale: "ja", symbol: "()"); |
+ var result = format.format(98765); |
+ expect(result, "()9.88\u4e07"); |
+ }); |
+} |
+ |
+testCurrency(String locale, num number, String expected, String expectedShort, |
+ {String currency}) { |
+ test("Compact simple currency for $locale, $number", () { |
+ var format = |
+ new NumberFormat.compactSimpleCurrency(locale: locale, name: currency); |
+ var result = format.format(number); |
+ expect(result, expected); |
+ var shortFormat = |
+ new NumberFormat.compactSimpleCurrency(locale: locale, name: currency); |
+ shortFormat.significantDigits = 1; |
+ var shortResult = shortFormat.format(number); |
+ expect(shortResult, expectedShort); |
+ }); |
+ test("Compact currency for $locale, $number", () { |
+ var symbols = { |
+ "ja": "¥", |
+ "en_US": r"$", |
+ "ru": "руб.", |
+ "it": "€", |
+ "CAD": r"$", |
+ "USD": r"$" |
+ }; |
+ var symbol = symbols[currency] ?? symbols[locale]; |
+ var format = new NumberFormat.compactCurrency( |
+ locale: locale, name: currency, symbol: symbol); |
+ var result = format.format(number); |
+ expect(result, expected); |
+ var shortFormat = new NumberFormat.compactCurrency( |
+ locale: locale, name: currency, symbol: symbol); |
+ shortFormat.significantDigits = 1; |
+ var shortResult = shortFormat.format(number); |
+ expect(shortResult, expectedShort); |
+ }); |
+} |
+ |
+// 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 problemLocalesShort = [ |
+ "am", // AM Suffixes differ, not sure why. |
+ "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", |
+ "fa", |
+ "fr_CA", // Several where PyICU isn't compacting. Expected: '988000000' |
+ // Actual: '988 M'. |
+ "gsw", // Suffixes disagree |
+ "in", // IN not compacting 54321, looks similar to tr. |
+ "id", // ID not compacting 54321, looks similar to tr. |
+ "ka", // K Slight difference in the suffix |
+ "kk", "mn", // We're picking the wrong pattern for 654321. |
+ "lo", "mk", "my", |
+ "pt_PT", // Seems to differ in appending mil or not after thousands. pt_BR |
+ // does it. |
+ "th", // TH Expected abbreviations as '1.09 พ.ล.' rather than '1.09 พ' |
+ "tr", // TR Doesn't have a 0B format, goes directly to 00B, as a result 54321 |
+ // just prints as 54321 |
+ "ur", // UR Fails one with Expected: '15 ٹریلین' Actual: '150 کھرب' |
+]; |
+ |
+/// Locales that have problems in the long format. |
+/// |
+/// These are mostly minor differences in the characters, and many I can't read, |
+/// but I'm suspicious many of them are essentially the difference between |
+/// million and millions, which we don't distinguish. That's definitely the case |
+/// with e.g. DE, but our data definitely has Millionen throughout. |
+/// |
+//TODO(alanknight): Narrow these down to particular numbers. Often it's just |
+// 999999. |
+var problemLocalesLong = [ |
+ "ar", |
+ "be", "bg", "bs", |
+ "ca", "cs", "da", "de", "de_AT", "de_CH", "el", "es", "es_419", "es_ES", |
+ "es_MX", "es_US", "et", "fi", |
+ "fil", // FIL is different, seems like a genuine difference in suffixes |
+ "fr", "fr_CA", "ga", "gl", |
+ "gsw", // GSW seems like we have long forms and pyICU doesn't |
+ "hr", "is", "it", "lo", // LO seems to be picking up a different pattern. |
+ "lt", "lv", "mk", |
+ "my", // Seems to come out in the reverse order |
+ "nb", "ne", "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", "sk", "sl", "sr", "sr_Latn", "sv", "te", "tl", |
+ "ur", |
+ "uk", |
+]; |
+ |
+void validate(String locale, List<List<String>> expected) { |
+ validateShort(locale, expected); |
+ validateLong(locale, expected); |
+} |
+ |
+/// Check each bit of test data against the short compact format, both |
+/// formatting and parsing. |
+void validateShort(String locale, List<List<String>> expected) { |
+ if (problemLocalesShort.contains(locale)) { |
+ print("Skipping problem locale '$locale' for SHORT compact number tests"); |
+ return; |
+ } |
+ var shortFormat = new NumberFormat.compact(locale: locale); |
+ for (var data in expected) { |
+ var number = num.parse(data.first); |
+ test("Validate $locale SHORT for ${data.first}", () { |
+ validateNumber(number, shortFormat, data[1]); |
+ }); |
+ var int64Number = new Int64(number); |
+ test("Validate Int64 SHORT on $locale for ${data.first}", () { |
+ validateNumber(int64Number, shortFormat, data[1]); |
+ }); |
+ // TODO(alanknight): Make this work for MicroMoney |
+ } |
+} |
+ |
+void validateLong(String locale, List<List<String>> expected) { |
+ if (problemLocalesLong.contains(locale)) { |
+ print("Skipping problem locale '$locale' for LONG compact number tests"); |
+ return; |
+ } |
+ var longFormat = new NumberFormat.compactLong(locale: locale); |
+ for (var data in expected) { |
+ var number = num.parse(data.first); |
+ test("Validate $locale LONG for ${data.first}", () { |
+ validateNumber(number, longFormat, data[2]); |
+ }); |
+ } |
+} |
+ |
+void validateNumber(number, NumberFormat format, String expected) { |
+ var formatted = format.format(number); |
+ var ok = closeEnough(formatted, expected); |
+ if (!ok) { |
+ expect( |
+ "$formatted ${formatted.codeUnits}", "$expected ${expected.codeUnits}"); |
+ } |
+ var parsed = format.parse(formatted); |
+ var rounded = roundForPrinting(number, format); |
+ expect((parsed - rounded) / rounded < 0.001, isTrue); |
+} |
+ |
+/// Duplicate a bit of the logic in formatting, where if we have a |
+/// number that will round to print differently depending on the number |
+/// of significant digits, we need to check that as well, e.g. |
+/// 999999 may print as 1M. |
+roundForPrinting(number, NumberFormat format) { |
+ var originalLength = NumberFormat.numberOfIntegerDigits(number); |
+ var additionalDigits = originalLength - format.significantDigits; |
+ 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; |
+ return rounded; |
+ } |
+ return number.toDouble(); |
+} |
+ |
+final _nbsp = 0xa0; |
+final _nbspString = new String.fromCharCode(_nbsp); |
+ |
+/// Return true if the strings are close enough to what we |
+/// expected to consider a pass. |
+/// |
+/// In particular, there seem to be minor differences between what PyICU is |
+/// currently producing and the CLDR data. So if the strings differ only in the |
+/// presence or absence of a period at the end or of a space between the number |
+/// and the suffix, consider it close enough and return true. |
+bool closeEnough(String result, String reference) { |
+ var expected = reference.replaceAll(' ', _nbspString); |
+ if (result == expected) { |
+ return true; |
+ } |
+ if ('$result.' == expected) { |
+ return true; |
+ } |
+ if (result == '$expected.') { |
+ return true; |
+ } |
+ if (_oneSpaceOnlyDifference(result, expected)) { |
+ return true; |
+ } |
+ return false; |
+} |
+ |
+/// Do the two strings differ only by a single space being |
+/// omitted in one of them. |
+/// |
+/// We assume non-breaking spaces because we |
+/// know that's what the Intl data uses. We already know the strings aren't |
+/// equal because that's checked first in the only caller. |
+bool _oneSpaceOnlyDifference(String result, String expected) { |
+ var resultWithoutSpaces = |
+ new String.fromCharCodes(result.codeUnits.where((x) => x != _nbsp)); |
+ var expectedWithoutSpaces = |
+ new String.fromCharCodes(expected.codeUnits.where((x) => x != _nbsp)); |
+ var resultDifference = result.length - resultWithoutSpaces.length; |
+ var expectedDifference = expected.length - expectedWithoutSpaces.length; |
+ return (resultWithoutSpaces == expectedWithoutSpaces && |
+ resultDifference <= 1 && |
+ expectedDifference <= 1); |
+} |