diff options
author | Neil Fuller <nfuller@google.com> | 2014-09-26 11:55:58 +0100 |
---|---|---|
committer | Neil Fuller <nfuller@google.com> | 2014-09-26 16:53:34 +0100 |
commit | 4e92b6265acb1d12109e311819dd64b27ec85df5 (patch) | |
tree | 07147102983f6cb93efe13b7914420a4a0ca2c0a /harmony-tests | |
parent | 37fcc48b63f95775d0304bf0bd3b9d7ff30e2232 (diff) | |
download | libcore-4e92b6265acb1d12109e311819dd64b27ec85df5.zip libcore-4e92b6265acb1d12109e311819dd64b27ec85df5.tar.gz libcore-4e92b6265acb1d12109e311819dd64b27ec85df5.tar.bz2 |
Rewrite tests and add tests that demonstrate a bug
The bug:
DecimalFormat.format() behavior is slightly
lossy around 15 decimal digits even without any
digit constraints.
This change isolates the test failures that result from
this bug to 2 test cases:
test_formatDouble_bug17656132
test_formatDouble_roundingProblemCases
Example of the bug:
Double: 999999999999999.9 is represented as an IEEE 754
value which is exactly decimal 999999999999999.875
When format(999999999999999.9) is called on a DecimalFormat
with large MaxIntegerDigit and MaxFractionDigit....
Correct answer: "999999999999999.9"
Actual answer: "1000000000000000"
By contrast Double.toString() prints 9.999999999999999E14
for Android and the RI (correctly).
The DecimalFormat is printing to 16 decimal digits: The
inclusion of the 16th digit implies slightly more precision
than IEEE 754 provides (~15.9 decimal digits for most of the
representable range).
However, the use of 16 decimal digits for outputting IEEE 754
appears consistent with Double.toString() and elsewhere.
Before printing, DecimalFormat appears to be rounding to
15 decimal digits internally (or something similar).
Parsing "1000000000000000" produces a different double
representation than the original double
(one that is closer to 1000000000000000 than
999999999999999.9). This is the bug - we just lost information.
We should be able to round-trip a double if there is no rounding
since every double is representable with decimal and we have
sufficient digits available to represent it (close enough) in
decimal.
Additional tests have been added to demonstrate the bug
and also demonstrate the (correct) formatting behavior when the
formatter is rounding.
test_formatDouble_maxFractionDigits: rounding at 1, 10, 14 digits
after the dp.
test_formatDouble_roundingTo15Digits: rounding at 15 total digits
overall
test_formatDouble_bug17656132: Demonstrates the bug concisely.
The test changes:
test_formatDouble_wideRange():
implicitly assumed that the any loss of accuracy
when a decimal string was turned into a double
would be undone when format() was called, and it would
always arrive back at the original string. The test
has been re-written here to use BigDecimal rather than
Double.parseDouble(), and to compare two doubles rather
than original string with the output from format().
The test was previously failing with the RI for 1E23:
the closest representable double to 1E23 is exactly
99999999999999991611392.
The value produces "99999999999999990000000" when formatted
with the RI and not "100000000000000000000000". On Android
it was passing for 1E23 because of bug 17656132 rounding
back to the original decimal value.
This test was previously failing on Android with 1E-309 because
below 1E-308 IEEE 754 starts losing precision and the closest
representable value is not close to the original string. The
test now isn't affected if the double being tested is not close
to the original decimal; it passes providing the can be round
tripped.
test_formatDouble_roundingProblemCases: Re-written like the
_wideRange test but continues to demonstrate the bug due to
the test values (intentionally) chosen.
Bug: 17656132
Change-Id: I7d81e38bd1f9dbfd1e1b2caa60a6bb16b871b925
Diffstat (limited to 'harmony-tests')
-rw-r--r-- | harmony-tests/src/test/java/org/apache/harmony/tests/java/text/DecimalFormatTest.java | 279 |
1 files changed, 221 insertions, 58 deletions
diff --git a/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/DecimalFormatTest.java b/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/DecimalFormatTest.java index ad96256..e0e04e8 100644 --- a/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/DecimalFormatTest.java +++ b/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/DecimalFormatTest.java @@ -1465,78 +1465,241 @@ public class DecimalFormatTest extends TestCase { formatTester.throwFailures(); } - public void test_formatDouble_wideRange() { + // Demonstrates that fraction digit rounding occurs as expected with 1, 10 and 14 fraction + // digits, using numbers that are well within the precision of IEEE 754. + public void test_formatDouble_maxFractionDigits() { final DecimalFormatSymbols dfs = new DecimalFormatSymbols(Locale.US); DecimalFormat format = new DecimalFormat("#0.#", dfs); + format.setGroupingUsed(false); format.setMaximumIntegerDigits(400); - format.setMaximumFractionDigits(400); - - for (int i = 0; i < 309; i++) { - String tval = "1"; - for (int j = 0; j < i; j++) { - tval += "0"; - } - double d = Double.parseDouble(tval); - String result = format.format(d); - assertEquals(i + ") e:" + tval + " r:" + result, tval, result); - } - for (int i = 0; i < 322; i++) { - String tval = "0."; - for (int j = 0; j < i; j++) { - tval += "0"; - } - tval += "1"; - double d = Double.parseDouble(tval); - String result = format.format(d); - assertEquals(i + ") e:" + tval + " r:" + result, tval, result); + format.setMaximumFractionDigits(1); + + assertEquals("1", format.format(0.99)); + assertEquals("1", format.format(0.95)); + assertEquals("0.9", format.format(0.94)); + assertEquals("0.9", format.format(0.90)); + + assertEquals("0.2", format.format(0.19)); + assertEquals("0.2", format.format(0.15)); + assertEquals("0.1", format.format(0.14)); + assertEquals("0.1", format.format(0.10)); + + format.setMaximumFractionDigits(10); + assertEquals("1", format.format(0.99999999999)); + assertEquals("1", format.format(0.99999999995)); + assertEquals("0.9999999999", format.format(0.99999999994)); + assertEquals("0.9999999999", format.format(0.99999999990)); + + assertEquals("0.1111111112", format.format(0.11111111119)); + assertEquals("0.1111111112", format.format(0.11111111115)); + assertEquals("0.1111111111", format.format(0.11111111114)); + assertEquals("0.1111111111", format.format(0.11111111110)); + + format.setMaximumFractionDigits(14); + assertEquals("1", format.format(0.999999999999999)); + assertEquals("1", format.format(0.999999999999995)); + assertEquals("0.99999999999999", format.format(0.999999999999994)); + assertEquals("0.99999999999999", format.format(0.999999999999990)); + + assertEquals("0.11111111111112", format.format(0.111111111111119)); + assertEquals("0.11111111111112", format.format(0.111111111111115)); + assertEquals("0.11111111111111", format.format(0.111111111111114)); + assertEquals("0.11111111111111", format.format(0.111111111111110)); + } + + // This demonstrates rounding at the 15th decimal digit. By setting the maximum fraction digits + // we force rounding at a point just below the full IEEE 754 precision. IEEE 754 should be + // precise to just above 15 decimal digits. + // df.format() with no limits always emits up to 16 decimal digits, slightly above what IEEE 754 + // can store precisely. + public void test_formatDouble_roundingTo15Digits() throws Exception { + final DecimalFormatSymbols dfs = new DecimalFormatSymbols(Locale.US); + DecimalFormat df = new DecimalFormat("#.#", dfs); + df.setMaximumIntegerDigits(400); + df.setGroupingUsed(false); + + df.setMaximumFractionDigits(0); + assertEquals("1000000000000000", df.format(999999999999999.9)); + df.setMaximumFractionDigits(1); + assertEquals("100000000000000", df.format(99999999999999.99)); + df.setMaximumFractionDigits(2); + assertEquals("10000000000000", df.format(9999999999999.999)); + df.setMaximumFractionDigits(3); + assertEquals("1000000000000", df.format(999999999999.9999)); + df.setMaximumFractionDigits(4); + assertEquals("100000000000", df.format(99999999999.99999)); + df.setMaximumFractionDigits(5); + assertEquals("10000000000", df.format(9999999999.999999)); + df.setMaximumFractionDigits(6); + assertEquals("1000000000", df.format(999999999.9999999)); + df.setMaximumFractionDigits(7); + assertEquals("100000000", df.format(99999999.99999999)); + df.setMaximumFractionDigits(8); + assertEquals("10000000", df.format(9999999.999999999)); + df.setMaximumFractionDigits(9); + assertEquals("1000000", df.format(999999.9999999999)); + df.setMaximumFractionDigits(10); + assertEquals("100000", df.format(99999.99999999999)); + df.setMaximumFractionDigits(11); + assertEquals("10000", df.format(9999.999999999999)); + df.setMaximumFractionDigits(12); + assertEquals("1000", df.format(999.9999999999999)); + df.setMaximumFractionDigits(13); + assertEquals("100", df.format(99.99999999999999)); + df.setMaximumFractionDigits(14); + assertEquals("10", df.format(9.999999999999999)); + df.setMaximumFractionDigits(15); + assertEquals("1", df.format(0.9999999999999999)); + } + + // This checks formatting over most of the representable IEEE 754 range using a formatter that + // should be performing no lossy rounding. + // It checks that the formatted double can be parsed to get the original double. + // IEEE 754 can represent values from (decimal) ~2.22507E−308 to ~1.79769E308 to full precision. + // Close to zero it can go down to 4.94066E-324 with a loss of precision. + // At the extremes of the double range this test will not be testing doubles that exactly + // represent powers of 10. The test is only interested in whether the doubles closest + // to powers of 10 that can be represented can each be turned into a string and read back again + // to arrive at the original double. + public void test_formatDouble_wideRange() throws Exception { + for (int i = -324; i < 309; i++) { + // Generate a decimal number we are interested in: 1 * 10^i + String stringForm = "1e" + i; + BigDecimal bigDecimal = new BigDecimal(stringForm); + + // Obtain the nearest double representation of the decimal number. + // This is lossy because going from BigDecimal -> double is inexact, but we should + // arrive at a number that is as close as possible to the decimal value. We assume that + // BigDecimal is capable of this, but it is not critical to this test if it gets it a + // little wrong, though it may mean we are testing a double value different from the one + // we thought we were. + double d = bigDecimal.doubleValue(); + + assertDecimalFormatIsLossless(d); } } - public void test_formatDouble_varyingScaleProblemCases() throws Exception { + // This test is a regression test for http://b/17656132. + // It checks hand-picked values can be formatted and parsed to get the original double. + // The formatter as configured should perform no rounding. + public void test_formatDouble_roundingProblemCases() throws Exception { + // Most of the double literals below are not exactly representable in IEEE 754 but + // it should not matter to this test. + assertDecimalFormatIsLossless(999999999999999.9); + assertDecimalFormatIsLossless(99999999999999.99); + assertDecimalFormatIsLossless(9999999999999.999); + assertDecimalFormatIsLossless(999999999999.9999); + assertDecimalFormatIsLossless(99999999999.99999); + assertDecimalFormatIsLossless(9999999999.999999); + assertDecimalFormatIsLossless(999999999.9999999); + assertDecimalFormatIsLossless(99999999.99999999); + assertDecimalFormatIsLossless(9999999.999999999); + assertDecimalFormatIsLossless(999999.9999999999); + assertDecimalFormatIsLossless(99999.99999999999); + assertDecimalFormatIsLossless(9999.999999999999); + assertDecimalFormatIsLossless(999.9999999999999); + assertDecimalFormatIsLossless(99.99999999999999); + assertDecimalFormatIsLossless(9.999999999999999); + assertDecimalFormatIsLossless(0.9999999999999999); + } + + // This test checks hand-picked values can be formatted and parsed to get the original double. + // The formatter as configured should perform no rounding. + // These numbers are not affected by http://b/17656132. + public void test_formatDouble_varyingScale() throws Exception { + // Most of the double literals below are not exactly representable in IEEE 754 but + // it should not matter to this test. + + assertDecimalFormatIsLossless(999999999999999.); + + assertDecimalFormatIsLossless(123456789012345.); + assertDecimalFormatIsLossless(12345678901234.5); + assertDecimalFormatIsLossless(1234567890123.25); + assertDecimalFormatIsLossless(999999999999.375); + assertDecimalFormatIsLossless(99999999999.0625); + assertDecimalFormatIsLossless(9999999999.03125); + assertDecimalFormatIsLossless(999999999.015625); + assertDecimalFormatIsLossless(99999999.0078125); + assertDecimalFormatIsLossless(9999999.00390625); + assertDecimalFormatIsLossless(999999.001953125); + assertDecimalFormatIsLossless(9999.00048828125); + assertDecimalFormatIsLossless(999.000244140625); + assertDecimalFormatIsLossless(99.0001220703125); + assertDecimalFormatIsLossless(9.00006103515625); + assertDecimalFormatIsLossless(0.000030517578125); + } + + private static void assertDecimalFormatIsLossless(double d) throws Exception { final DecimalFormatSymbols dfs = new DecimalFormatSymbols(Locale.US); DecimalFormat format = new DecimalFormat("#0.#", dfs); + format.setGroupingUsed(false); format.setMaximumIntegerDigits(400); format.setMaximumFractionDigits(400); - assertEquals("999999999999999", format.format(999999999999999.)); - assertEquals("999999999999999.9", format.format(999999999999999.9)); - assertEquals("99999999999999.98", format.format(99999999999999.99)); - assertEquals("9999999999999.998", format.format(9999999999999.999)); - assertEquals("999999999999.9999", format.format(999999999999.9999)); - assertEquals("99999999999.99998", format.format(99999999999.99999)); - assertEquals("9999999999.999998", format.format(9999999999.999999)); - assertEquals("999999999.9999999", format.format(999999999.9999999)); - assertEquals("99999999.99999999", format.format(99999999.99999999)); - assertEquals("9999999.999999998", format.format(9999999.999999999)); - assertEquals("99999.99999999999", format.format(99999.99999999999)); - assertEquals("9999.999999999998", format.format(9999.999999999999)); - assertEquals("999.9999999999999", format.format(999.9999999999999)); - assertEquals("99.99999999999999", format.format(99.99999999999999)); - assertEquals("9.999999999999998", format.format(9.999999999999999)); - assertEquals("0.9999999999999999", format.format(.9999999999999999)); + // Every floating point binary can be represented exactly in decimal if you have enough + // digits. This shows the value actually being tested. + String testId = "decimalValue: " + new BigDecimal(d); + + // As a sanity check we try out parseDouble() with the string generated by + // Double.toString(). Strictly speaking Double.toString() is probably not guaranteed to be + // lossless, but in reality it probably is, or at least is close enough. + assertDoubleEqual( + testId + " failed parseDouble(toString()) sanity check", + d, Double.parseDouble(Double.toString(d))); + + // Format the number: If this is lossy it is a problem. We are trying to check that it + // doesn't lose any unnecessary precision. + String result = format.format(d); + + // Here we use Double.parseDouble() which should able to parse a number we know was + // representable as a double into the original double. If parseDouble() is not implemented + // correctly the test is invalid. + double doubleParsed = Double.parseDouble(result); + assertDoubleEqual(testId + " (format() produced " + result + ")", + d, doubleParsed); + + // For completeness we try to parse using the formatter too. If this fails but the format + // above didn't it may be a problem with parse(), or with format() that we didn't spot. + assertDoubleEqual(testId + " failed parse(format()) check", + d, format.parse(result).doubleValue()); } - public void test_formatDouble_varyingScale() throws Exception { - final DecimalFormatSymbols dfs = new DecimalFormatSymbols(Locale.US); - DecimalFormat format = new DecimalFormat("#0.#", dfs); - format.setMaximumIntegerDigits(400); - format.setMaximumFractionDigits(400); + private static void assertDoubleEqual(String message, double d, double doubleParsed) { + assertEquals(message, + createPrintableDouble(d),createPrintableDouble(doubleParsed)); + } - assertEquals("123456789012345", format.format(123456789012345.)); - assertEquals("12345678901234.5", format.format(12345678901234.5)); - assertEquals("1234567890123.25", format.format(1234567890123.25)); - assertEquals("999999999999.375", format.format(999999999999.375)); - assertEquals("99999999999.0625", format.format(99999999999.0625)); - assertEquals("9999999999.03125", format.format(9999999999.03125)); - assertEquals("999999999.015625", format.format(999999999.015625)); - assertEquals("99999999.0078125", format.format(99999999.0078125)); - assertEquals("9999999.00390625", format.format(9999999.00390625)); - assertEquals("999999.001953125", format.format(999999.001953125)); - assertEquals("9999.00048828125", format.format(9999.00048828125)); - assertEquals("999.000244140625", format.format(999.000244140625)); - assertEquals("99.0001220703125", format.format(99.0001220703125)); - assertEquals("9.00006103515625", format.format(9.00006103515625)); - assertEquals("0.000030517578125", format.format(0.000030517578125)); + private static String createPrintableDouble(double d) { + return Double.toString(d) + "(" + Long.toHexString(Double.doubleToRawLongBits(d)) + ")"; + } + + // Concise demonstration of http://b/17656132 using hard-coded expected values. + public void test_formatDouble_bug17656132() { + final DecimalFormatSymbols dfs = new DecimalFormatSymbols(Locale.US); + DecimalFormat df = new DecimalFormat("#0.#", dfs); + df.setGroupingUsed(false); + df.setMaximumIntegerDigits(400); + df.setMaximumFractionDigits(400); + + // The expected values below come from the RI and are correct 16 decimal digit + // representations of the formatted value. Android does something different. + // The decimal value given in each comment is the actual double value as represented by + // IEEE 754. See new BigDecimal(double). + + // double: 999999999999999.9 is decimal 999999999999999.875 + assertEquals("999999999999999.9", df.format(999999999999999.9)); + // double: 99999999999999.98 is decimal 99999999999999.984375 + assertEquals("99999999999999.98", df.format(99999999999999.98)); + // double 9999999999999.998 is decimal 9999999999999.998046875 + assertEquals("9999999999999.998", df.format(9999999999999.998)); + // double 999999999999.9999 is decimal 999999999999.9998779296875 + assertEquals("999999999999.9999", df.format(999999999999.9999)); + // double 99999999999.99998 is decimal 99999999999.9999847412109375 + assertEquals("99999999999.99998", df.format(99999999999.99998)); + // double 9999999999.999998 is decimal 9999999999.9999980926513671875 + assertEquals("9999999999.999998", df.format(9999999999.999998)); + // double 1E23 is decimal 99999999999999991611392 + assertEquals("9999999999999999", df.format(1E23)); } public void test_getDecimalFormatSymbols() { |