blob: b2961cdf53037912e0c15db164c9a0c55803dfac [file] [log] [blame]
/// Copyright (c) 2012, 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.
library number_format_test;
import 'package:test/test.dart';
import 'package:intl/number_symbols_data.dart';
import 'package:intl/intl.dart';
import 'number_test_data.dart';
/// Tests the Numeric formatting library in dart.
var testNumbersWeCanReadBack = {
'-1': -1,
'-2': -2.0,
'-0.01': -0.01,
'-1.23': -1.23,
'0.001': 0.001,
'0.01': 0.01,
'0.1': 0.1,
'1': 1,
'2': 2.0,
'10': 10,
'100': 100,
'1,000': 1000,
'2,000,000,000,000': 2000000000000,
'0.123': 0.123,
'1,234': 1234.0,
'1.234': 1.234,
'1.23': 1.230,
'NaN': 0.0 / 0.0,
'∞': 1.0 / 0.0,
'-∞': -1.0 / 0.0,
};
/// Test numbers that we can't parse because we lose precision in formatting.
var testNumbersWeCannotReadBack = {
'3.142': 3.1415926535897932,
'-1.234': -1.2342,
'-1.235': -1.2348,
'1.234': 1.2342,
'1.235': 1.2348
};
var testExponential = const {'1E-3': 0.001, '1E-2': 0.01, '1.23E0': 1.23};
// TODO(alanknight): Test against currency, which requires generating data
// for the three different forms that this now supports.
// TODO(alanknight): Test against scientific, which requires significant
// digit support.
List<NumberFormat> standardFormats(String locale) {
return [
NumberFormat.decimalPattern(locale),
NumberFormat.percentPattern(locale),
];
}
Map<String, num> get testNumbers =>
Map.from(testNumbersWeCanReadBack)..addAll(testNumbersWeCannotReadBack);
void runTests(Map<String, num> allTestNumbers) {
// For data from a list of locales, run each locale's data as a separate
// test so we can see exactly which ones pass or fail. The test data is
// hard-coded as printing 123, -12.3, %12,300, -1,230% in each locale.
var mainList = numberTestData;
var sortedLocales = List.from(numberFormatSymbols.keys);
sortedLocales.sort((a, b) => a.compareTo(b));
for (var locale in sortedLocales) {
var testFormats = standardFormats(locale);
var testLength = (testFormats.length * 3) + 1;
var list = mainList.take(testLength).iterator;
list.moveNext();
if (locale == list.current) {
mainList = mainList.skip(testLength).toList();
testAgainstIcu(locale, testFormats, list);
} else if (!numberFormatSymbols.containsKey(list.current)) {
throw Exception(
'Test locale ${list.current} is lacking in numberFormatSymbols.');
} else {
print('No unit tests found in numberTestData for locale $locale.');
}
}
if (mainList[0] != 'END') {
throw Exception(
'Test locale ${mainList[0]} is lacking in numberFormatSymbols.');
}
test('Simple set of numbers', () {
var number = NumberFormat();
for (var x in allTestNumbers.keys) {
var formatted = number.format(allTestNumbers[x]);
expect(formatted, x);
if (!testNumbersWeCannotReadBack.containsKey(x)) {
var readBack = number.parse(formatted);
// Even among ones we can read back, we can't test NaN for equality.
if (allTestNumbers[x].isNaN) {
expect(readBack.isNaN, isTrue);
} else {
expect(readBack, allTestNumbers[x]);
}
}
}
});
test('Padding left', () {
var expected = [
'1',
'1',
'01',
'001',
'0,001',
'00,001',
'000,001',
'0,000,001'
];
for (var i = 0; i < 7; i++) {
var f = NumberFormat.decimalPattern();
f.minimumIntegerDigits = i;
expect(f.format(1), expected[i], reason: 'minimumIntegerDigits: $i');
}
});
test('maximumIntegerDigits does not do much', () {
var expected = [
'9,876,543,210',
'9,876,543,210',
'9,876,543,210',
'9,876,543,210',
'9,876,543,210',
'9,876,543,210',
];
for (var i = 0; i < expected.length; i++) {
var f = new NumberFormat.decimalPattern();
f.maximumIntegerDigits = i;
expect(f.format(9876543210), expected[i],
reason: 'maximumIntegerDigits: $i');
}
});
test('Padding right', () {
var expected = [
'1',
'1.0',
'1.00',
'1.000',
'1.0000',
'1.00000',
'1.000000',
];
for (var i = 0; i < 6; i++) {
var f = new NumberFormat.decimalPattern();
f.minimumFractionDigits = i;
if (i > f.maximumFractionDigits) f.maximumFractionDigits = i;
expect(f.format(1), expected[i],
reason: 'minimumFractionDigits: $i, '
'maximumFractionDigits: ${f.maximumFractionDigits}');
}
});
test('Rounding/truncating fractions', () {
var expected = [
'9',
'9.1',
'9.12',
'9.123',
'9.1235',
'9.12346',
'9.123457',
'9.1234568',
'9.12345679',
'9.123456789',
'9.123456789',
'9.123456789',
];
for (var i = 0; i < expected.length; i++) {
var f = new NumberFormat.decimalPattern();
f.maximumFractionDigits = i;
expect(f.format(9.123456789), expected[i],
reason: 'maximumFractionDigits: $i');
}
});
test('Exponential form', () {
var f = NumberFormat('#.###E0');
for (var x in testExponential.keys) {
var formatted = f.format(testExponential[x]);
expect(formatted, x);
var readBack = f.parse(formatted);
expect(testExponential[x], readBack);
}
});
test('Exponential form with minimumExponentDigits', () {
var expected = [
'3.21E3',
'3.21E3',
'3.21E03',
'3.21E003',
];
for (var i = 0; i < expected.length; i++) {
var f = new NumberFormat("#.###E0");
f.minimumExponentDigits = i;
expect(f.format(3210), expected[i], reason: 'minimumExponentDigits: $i');
}
});
test('Significant Digits', () {
var expected = [
'00,000,000',
'10,000,000',
'9,900,000',
'9,880,000',
'9,877,000',
'9,876,500',
'9,876,540',
'9,876,543',
'9,876,543.2',
'9,876,543.21',
'9,876,543.21',
'9,876,543.2101',
'9,876,543.21012',
'9,876,543.21012',
];
for (var i = 0; i < expected.length; i++) {
var f = new NumberFormat.decimalPattern();
f.significantDigits = i;
expect(f.format(9876543.21012), expected[i],
reason: 'significantDigits: $i');
}
});
test('Percent with no decimals and no integer part', () {
var number = NumberFormat('#%');
var formatted = number.format(0.12);
expect(formatted, '12%');
var readBack = number.parse(formatted);
expect(0.12, readBack);
});
// We can't do these in the normal tests because those also format the
// numbers and we're reading them in a format where they won't print
// back the same way.
test('Parsing modifiers,e.g. percent, in the base format', () {
var number = NumberFormat();
var modified = {'12%': 0.12, '12\u2030': 0.012};
modified.addAll(testExponential);
for (var x in modified.keys) {
var parsed = number.parse(x);
expect(parsed, modified[x]);
}
});
test('Explicit currency name', () {
var amount = 1000000.32;
var usConvention = NumberFormat.currency(locale: 'en_US', symbol: '€');
var formatted = usConvention.format(amount);
expect(formatted, '€1,000,000.32');
var readBack = usConvention.parse(formatted);
expect(readBack, amount);
// ignore: deprecated_member_use_from_same_package
var swissConvention = NumberFormat.currencyPattern('de_CH', r'$');
formatted = swissConvention.format(amount);
var nbsp = String.fromCharCode(0xa0);
var backquote = String.fromCharCode(0x2019);
//ignore: prefer_interpolation_to_compose_strings
expect(
formatted,
//ignore: prefer_interpolation_to_compose_strings
r'$' + nbsp + '1' + backquote + '000' + backquote + '000.32');
readBack = swissConvention.parse(formatted);
expect(readBack, amount);
// ignore: deprecated_member_use_from_same_package
var italianSwiss = NumberFormat.currencyPattern('it_CH', r'$');
formatted = italianSwiss.format(amount);
expect(
formatted,
//ignore: prefer_interpolation_to_compose_strings
r'$' + nbsp + '1' + backquote + '000' + backquote + '000.32');
readBack = italianSwiss.parse(formatted);
expect(readBack, amount);
/// Verify we can leave off the currency and it gets filled in.
var plainSwiss = NumberFormat.currency(locale: 'de_CH');
formatted = plainSwiss.format(amount);
expect(
formatted,
//ignore: prefer_interpolation_to_compose_strings
r'CHF' + nbsp + '1' + backquote + '000' + backquote + '000.32');
readBack = plainSwiss.parse(formatted);
expect(readBack, amount);
// Verify that we can pass null in order to specify the currency symbol
// but use the default locale.
// ignore: deprecated_member_use_from_same_package
var defaultLocale = NumberFormat.currencyPattern(null, 'Smurfs');
formatted = defaultLocale.format(amount);
// We don't know what the exact format will be, but it should have Smurfs.
expect(formatted.contains('Smurfs'), isTrue);
readBack = defaultLocale.parse(formatted);
expect(readBack, amount);
});
test('Delta percent format', () {
var f = NumberFormat('+#,##0%;-#,##0%');
expect(f.format(-0.07), '-7%');
expect(f.format(0.12), '+12%');
});
test('Unparseable', () {
var format = NumberFormat.currency();
expect(() => format.parse('abcdefg'), throwsFormatException);
expect(() => format.parse(''), throwsFormatException);
expect(() => format.parse('1.0zzz'), throwsFormatException);
expect(() => format.parse('-∞+1'), throwsFormatException);
});
var digitsCheck = {
0: '@4',
1: '@4.3',
2: '@4.32',
3: '@4.322',
4: '@4.3220',
};
test('Decimal digits', () {
var amount = 4.3219876;
for (var digits in digitsCheck.keys) {
var f = NumberFormat.currency(
locale: 'en_US', symbol: '@', decimalDigits: digits);
var formatted = f.format(amount);
expect(formatted, digitsCheck[digits]);
}
var defaultFormat = NumberFormat.currency(locale: 'en_US', symbol: '@');
var formatted = defaultFormat.format(amount);
expect(formatted, digitsCheck[2]);
var jpyUs =
NumberFormat.currency(locale: 'en_US', name: 'JPY', symbol: '@');
formatted = jpyUs.format(amount);
expect(formatted, digitsCheck[0]);
var jpyJa = NumberFormat.currency(locale: 'ja', name: 'JPY', symbol: '@');
formatted = jpyJa.format(amount);
expect(formatted, digitsCheck[0]);
var jpySimple = NumberFormat.simpleCurrency(locale: 'ja', name: 'JPY');
formatted = jpySimple.format(amount);
expect(formatted, '¥4');
var jpyLower =
NumberFormat.currency(locale: 'en_US', name: 'jpy', symbol: '@');
formatted = jpyLower.format(amount);
expect(formatted, digitsCheck[0]);
var tnd = NumberFormat.currency(name: 'TND', symbol: '@');
formatted = tnd.format(amount);
expect(formatted, digitsCheck[3]);
});
testSimpleCurrencySymbols();
test('Padding digits with non-ascii zero', () {
var format = NumberFormat('000', 'ar_EG');
var padded = format.format(0);
expect(padded, '٠٠٠');
});
// Exercise a custom pattern. There's not actually much logic here, so just
// validate that the custom pattern is in fact being used.
test('Custom currency pattern', () {
var format =
NumberFormat.currency(name: 'XYZZY', customPattern: '[\u00a4][#,##.#]');
var text = format.format(12345.67);
expect(text, '[XYZZY][1,23,45.67]');
});
}
String stripExtras(String input) {
// Some of these results from CLDR have a leading LTR/RTL indicator,
// and/or Arabic letter indicator,
// which we don't want. We also treat the difference between Unicode
// minus sign (2212) and hyphen-minus (45) as not significant.
return input
.replaceAll('\u200e', '')
.replaceAll('\u200f', '')
.replaceAll('\u061c', '')
.replaceAll('\u2212', '-');
}
void testAgainstIcu(locale, List<NumberFormat> testFormats, list) {
test('Test against ICU data for $locale', () {
for (var format in testFormats) {
var formatted = format.format(123);
var negative = format.format(-12.3);
var large = format.format(1234567890);
var expected = (list..moveNext()).current;
expect(formatted, expected);
var expectedNegative = (list..moveNext()).current;
expect(stripExtras(negative), stripExtras(expectedNegative));
var expectedLarge = (list..moveNext()).current;
expect(large, expectedLarge);
var readBack = format.parse(formatted);
expect(readBack, 123);
var readBackNegative = format.parse(negative);
expect(readBackNegative, -12.3);
var readBackLarge = format.parse(large);
expect(readBackLarge, 1234567890);
}
});
}
void testSimpleCurrencySymbols() {
var currencies = ['USD', 'CAD', 'EUR', 'CRC', null];
// Note that these print using the simple symbol as if we were in a
// a locale where that currency symbol is well understood. So we
// expect Canadian dollars printed as $, even though our locale is
// en_US, and this would confuse users.
var simple = currencies.map((currency) =>
NumberFormat.simpleCurrency(locale: 'en_US', name: currency));
var expectedSimple = [r'$', r'$', '\u20ac', '\u20a1', r'$'];
// These will always print as the global name, regardless of locale
var global = currencies.map(
(currency) => NumberFormat.currency(locale: 'en_US', name: currency));
var expectedGlobal = currencies.map((curr) => curr ?? 'USD').toList();
testCurrencySymbolsFor(expectedGlobal, global, 'global');
testCurrencySymbolsFor(expectedSimple, simple, 'simple');
}
void testCurrencySymbolsFor(expected, formats, name) {
var amount = 1000000.32;
Map<Object, NumberFormat>.fromIterables(expected, formats)
.forEach((expected, NumberFormat format) {
test('Test $name ${format.currencyName}', () {
// Allow for currencies with different fraction digits, e.g. CRC.
var maxDigits = format.maximumFractionDigits;
var rounded = maxDigits == 0 ? amount.round() : amount;
var fractionDigits = (amount - rounded) < 0.00001 ? '.32' : '';
var formatted = format.format(rounded);
expect(formatted, '${expected}1,000,000$fractionDigits');
var parsed = format.parse(formatted);
expect(parsed, rounded);
});
});
}