diff --git a/src/test/java/com/fasterxml/jackson/core/io/jdk/DoubleToStringTest.java b/src/test/java/com/fasterxml/jackson/core/io/jdk/DoubleToStringTest.java new file mode 100644 index 0000000000..5b53baf8c7 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/io/jdk/DoubleToStringTest.java @@ -0,0 +1,76 @@ +package com.fasterxml.jackson.core.io.jdk; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public abstract class DoubleToStringTest { + abstract String f(double f); + + private void assertD2sEquals(String expected, double f) { + assertEquals(expected, f(f)); + } + + @Test + public void simpleCases() { + assertD2sEquals("0.0", 0); + assertD2sEquals("-0.0", Double.longBitsToDouble(0x8000000000000000L)); + assertD2sEquals("1.0", 1.0d); + assertD2sEquals("-1.0", -1.0d); + assertD2sEquals("NaN", Double.NaN); + assertD2sEquals("Infinity", Double.POSITIVE_INFINITY); + assertD2sEquals("-Infinity", Double.NEGATIVE_INFINITY); + } + + @Test + public void switchToSubnormal() { + assertD2sEquals("2.2250738585072014E-308", Double.longBitsToDouble(0x0010000000000000L)); + } + + /** + * Floating point values in the range 1.0E-3 <= x < 1.0E7 have to be printed + * without exponent. This test checks the values at those boundaries. + */ + @Test + public void boundaryConditions() { + // x = 1.0E7 + assertD2sEquals("1.0E7", 1.0E7d); + // x < 1.0E7 + assertD2sEquals("9999999.999999998", 9999999.999999998d); + // x = 1.0E-3 + assertD2sEquals("0.001", 0.001d); + // x < 1.0E-3 + assertD2sEquals("9.999999999999998E-4", 0.0009999999999999998d); + } + + @Test + public void minAndMax() { + assertD2sEquals("1.7976931348623157E308", Double.longBitsToDouble(0x7fefffffffffffffL)); + assertD2sEquals("4.9E-324", Double.longBitsToDouble(1)); + } + + @Test + public void roundingModeEven() { + //result differs to Schubfach + assertD2sEquals("-2.1098088986959632E16", -2.109808898695963E16); + } + + @Test + public void regressionTest() { + assertD2sEquals("4.940656E-318", 4.940656E-318d); + assertD2sEquals("1.18575755E-316", 1.18575755E-316d); + assertD2sEquals("2.989102097996E-312", 2.989102097996E-312d); + assertD2sEquals("9.0608011534336E15", 9.0608011534336E15d); + //next result differs to Schubfach + assertD2sEquals("4.7083560247115121E18", 4.708356024711512E18); + assertD2sEquals("9.409340012568248E18", 9.409340012568248E18); + // This number naively requires 65 bit for the intermediate results if we reduce the lookup + // table by half. This checks that we don't lose any information in that case. + assertD2sEquals("1.8531501765868567E21", 1.8531501765868567E21); + assertD2sEquals("-3.347727380279489E33", -3.347727380279489E33); + // Discovered by Andriy Plokhotnyuk, see #29. + assertD2sEquals("1.9430376160308388E16", 1.9430376160308388E16); + assertD2sEquals("-6.9741824662760956E19", -6.9741824662760956E19); + assertD2sEquals("4.3816050601147837E18", 4.3816050601147837E18); + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/io/jdk/FloatToStringTest.java b/src/test/java/com/fasterxml/jackson/core/io/jdk/FloatToStringTest.java new file mode 100644 index 0000000000..6c9bd08b68 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/io/jdk/FloatToStringTest.java @@ -0,0 +1,113 @@ +package com.fasterxml.jackson.core.io.jdk; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public abstract class FloatToStringTest { + abstract String f(float f); + + private void assertF2sEquals(String expected, float f) { + assertEquals(expected, f(f)); + } + + @Test + public void simpleCases() { + assertF2sEquals("0.0", 0); + assertF2sEquals("-0.0", Float.intBitsToFloat(0x80000000)); + assertF2sEquals("1.0", 1.0f); + assertF2sEquals("-1.0", -1f); + assertF2sEquals("NaN", Float.NaN); + assertF2sEquals("Infinity", Float.POSITIVE_INFINITY); + assertF2sEquals("-Infinity", Float.NEGATIVE_INFINITY); + } + + @Test + public void switchToSubnormal() { + //next one different to Schubfach + assertF2sEquals("1.17549435E-38", Float.intBitsToFloat(0x00800000)); + } + + /** + * Floating point values in the range 1.0E-3 <= x < 1.0E7 have to be printed + * without exponent. This test checks the values at those boundaries. + */ + @Test + public void boundaryConditions() { + // x = 1.0E7 + assertF2sEquals("1.0E7", 1.0E7f); + // x < 1.0E7 + assertF2sEquals("9999999.0", 9999999.0f); + // x = 1.0E-3 + assertF2sEquals("0.001", 0.001f); + // x < 1.0E-3 + assertF2sEquals("9.999999E-4", 0.0009999999f); + } + + @Test + public void minAndMax() { + assertF2sEquals("3.4028235E38", Float.intBitsToFloat(0x7f7fffff)); + assertF2sEquals("1.4E-45", Float.intBitsToFloat(0x00000001)); + } + + @Test + public void roundingModeEven() { + //all different to Schubfach + assertF2sEquals("3.3554448E7", 3.3554448E7f); + assertF2sEquals("8.9999995E9", 8.999999E9f); + assertF2sEquals("3.4366718E10", 3.4366717E10f); + } + + @Test + public void roundingEvenIfTied() { + assertF2sEquals("0.33007812", 0.33007812f); + } + + @Test + public void looksLikePow5() { + // These are all floating point numbers where the mantissa is a power of 5, + // and the exponent is in the range such that q = 10. + assertF2sEquals("6.7108864E17", Float.intBitsToFloat(0x5D1502F9)); + //next 2 are slightly different to Schubfach + assertF2sEquals("1.34217728E18", Float.intBitsToFloat(0x5D9502F9)); + assertF2sEquals("2.68435456E18", Float.intBitsToFloat(0x5E1502F9)); + } + + @Test + public void regressionTest() { + assertF2sEquals("4.7223665E21", 4.7223665E21f); + assertF2sEquals("8388608.0", 8388608.0f); + assertF2sEquals("1.6777216E7", 1.6777216E7f); + assertF2sEquals("3.3554436E7", 3.3554436E7f); + assertF2sEquals("6.7131496E7", 6.7131496E7f); + assertF2sEquals("1.9310392E-38", 1.9310392E-38f); + assertF2sEquals("-2.47E-43", -2.47E-43f); + assertF2sEquals("1.993244E-38", 1.993244E-38f); + assertF2sEquals("4103.9004", 4103.9003f); + assertF2sEquals("5.3399997E9", 5.3399997E9f); + assertF2sEquals("6.0898E-39", 6.0898E-39f); + assertF2sEquals("0.0010310042", 0.0010310042f); + //next one is more accurate than Schubfach + assertF2sEquals("2.8823261E17", 2.8823261E17f); + assertF2sEquals("7.038531E-26", 7.038531E-26f); + //next 2 are more accurate than Schubfach + assertF2sEquals("9.2234038E17", 9.2234038E17f); + assertF2sEquals("6.7108872E7", 6.7108872E7f); + //next one matches Schubfach but not Ryu (Ryu is more accurate) + assertF2sEquals("9.8E-45", 1.0E-44f); + //next one is less accurate than Schubfach + assertF2sEquals("2.81602484E14", 2.816025E14f); + assertF2sEquals("9.223372E18", 9.223372E18f); + assertF2sEquals("1.5846086E29", 1.5846085E29f); + assertF2sEquals("1.1811161E19", 1.1811161E19f); + //next one is less accurate than Schubfach + assertF2sEquals("5.3687091E18", 5.368709E18f); + assertF2sEquals("4.6143166E18", 4.6143165E18f); + assertF2sEquals("0.007812537", 0.007812537f); + assertF2sEquals("1.4E-45", 1.4E-45f); + assertF2sEquals("1.18697725E20", 1.18697724E20f); + assertF2sEquals("1.00014165E-36", 1.00014165E-36f); + assertF2sEquals("200.0", 200f); + assertF2sEquals("3.3554432E7", 3.3554432E7f); + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/io/jdk/JdkDoubleTest.java b/src/test/java/com/fasterxml/jackson/core/io/jdk/JdkDoubleTest.java new file mode 100644 index 0000000000..29074565be --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/io/jdk/JdkDoubleTest.java @@ -0,0 +1,12 @@ +package com.fasterxml.jackson.core.io.jdk; + +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class JdkDoubleTest extends DoubleToStringTest { + @Override + String f(double f) { + return Double.toString(f); + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/io/jdk/JdkFloatTest.java b/src/test/java/com/fasterxml/jackson/core/io/jdk/JdkFloatTest.java new file mode 100644 index 0000000000..3eb86d9269 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/io/jdk/JdkFloatTest.java @@ -0,0 +1,12 @@ +package com.fasterxml.jackson.core.io.jdk; + +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class JdkFloatTest extends FloatToStringTest { + @Override + String f(float f) { + return Float.toString(f); + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/io/schubfach/DoubleToDecimalChecker.java b/src/test/java/com/fasterxml/jackson/core/io/schubfach/DoubleToDecimalChecker.java new file mode 100644 index 0000000000..52f48eadce --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/io/schubfach/DoubleToDecimalChecker.java @@ -0,0 +1,303 @@ +/* + * Copyright 2018-2020 Raffaello Giulietti + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.fasterxml.jackson.core.io.schubfach; + +import java.math.BigDecimal; +import java.util.Random; + +import static java.lang.Double.*; +import static java.lang.Long.numberOfTrailingZeros; +import static java.lang.StrictMath.scalb; +import static com.fasterxml.jackson.core.io.schubfach.MathUtils.flog10pow2; + +public class DoubleToDecimalChecker extends ToDecimalChecker { + + static final int P = + numberOfTrailingZeros(doubleToRawLongBits(3)) + 2; + private static final int W = (SIZE - 1) - (P - 1); + static final int Q_MIN = (-1 << W - 1) - P + 3; + static final int Q_MAX = (1 << W - 1) - P; + static final long C_MIN = 1L << P - 1; + static final long C_MAX = (1L << P) - 1; + + static final int K_MIN = flog10pow2(Q_MIN); + static final int K_MAX = flog10pow2(Q_MAX); + static final int H = flog10pow2(P) + 2; + + static final double MIN_VALUE = scalb(1.0, Q_MIN); + static final double MIN_NORMAL = scalb((double) C_MIN, Q_MIN); + static final double MAX_VALUE = scalb((double) C_MAX, Q_MAX); + + static final int E_MIN = e(MIN_VALUE); + static final int E_MAX = e(MAX_VALUE); + + static final long C_TINY = cTiny(Q_MIN, K_MIN); + + private double v; + private final long originalBits; + + private DoubleToDecimalChecker(double v, String s) { + super(s); + this.v = v; + originalBits = doubleToRawLongBits(v); + } + + @Override + BigDecimal toBigDecimal() { + return new BigDecimal(v); + } + + @Override + boolean recovers(BigDecimal b) { + return b.doubleValue() == v; + } + + @Override + boolean recovers(String s) { + return parseDouble(s) == v; + } + + @Override + String hexBits() { + return String.format("0x%01X__%03X__%01X_%04X_%04X_%04X", + (int) (originalBits >>> 63) & 0x1, + (int) (originalBits >>> 52) & 0x7FF, + (int) (originalBits >>> 48) & 0xF, + (int) (originalBits >>> 32) & 0xFFFF, + (int) (originalBits >>> 16) & 0xFFFF, + (int) originalBits & 0xFFFF); + } + + @Override + int minExp() { + return E_MIN; + } + + @Override + int maxExp() { + return E_MAX; + } + + @Override + int maxLen10() { + return H; + } + + @Override + boolean isZero() { + return v == 0; + } + + @Override + boolean isInfinity() { + return v == POSITIVE_INFINITY; + } + + @Override + void negate() { + v = -v; + } + + @Override + boolean isNegative() { + return originalBits < 0; + } + + @Override + boolean isNaN() { + return Double.isNaN(v); + } + + static void toDec(double v) { +// String s = Double.toString(v); + String s = DoubleToDecimal.toString(v); + new DoubleToDecimalChecker(v, s).validate(); + } + + /* + There are tons of doubles that are rendered incorrectly by the JDK. + While the renderings correctly round back to the original value, + they are longer than needed or are not the closest decimal to the double. + Here are just a very few examples. + */ + static final String[] Anomalies = { + // JDK renders these, and others, with 18 digits! + "2.82879384806159E17", "1.387364135037754E18", + "1.45800632428665E17", + + // JDK renders these longer than needed. + "1.6E-322", "6.3E-322", + "7.3879E20", "2.0E23", "7.0E22", "9.2E22", + "9.5E21", "3.1E22", "5.63E21", "8.41E21", + + // JDK does not render these, and many others, as the closest. + "9.9E-324", "9.9E-323", + "1.9400994884341945E25", "3.6131332396758635E25", + "2.5138990223946153E25", + }; + + /* + Values are from + Paxson V, "A Program for Testing IEEE Decimal-Binary Conversion" + tables 3 and 4 + */ + static final double[] PaxsonSignificands = { + 8_511_030_020_275_656L, + 5_201_988_407_066_741L, + 6_406_892_948_269_899L, + 8_431_154_198_732_492L, + 6_475_049_196_144_587L, + 8_274_307_542_972_842L, + 5_381_065_484_265_332L, + 6_761_728_585_499_734L, + 7_976_538_478_610_756L, + 5_982_403_858_958_067L, + 5_536_995_190_630_837L, + 7_225_450_889_282_194L, + 7_225_450_889_282_194L, + 8_703_372_741_147_379L, + 8_944_262_675_275_217L, + 7_459_803_696_087_692L, + 6_080_469_016_670_379L, + 8_385_515_147_034_757L, + 7_514_216_811_389_786L, + 8_397_297_803_260_511L, + 6_733_459_239_310_543L, + 8_091_450_587_292_794L, + + 6_567_258_882_077_402L, + 6_712_731_423_444_934L, + 6_712_731_423_444_934L, + 5_298_405_411_573_037L, + 5_137_311_167_659_507L, + 6_722_280_709_661_868L, + 5_344_436_398_034_927L, + 8_369_123_604_277_281L, + 8_995_822_108_487_663L, + 8_942_832_835_564_782L, + 8_942_832_835_564_782L, + 8_942_832_835_564_782L, + 6_965_949_469_487_146L, + 6_965_949_469_487_146L, + 6_965_949_469_487_146L, + 7_487_252_720_986_826L, + 5_592_117_679_628_511L, + 8_887_055_249_355_788L, + 6_994_187_472_632_449L, + 8_797_576_579_012_143L, + 7_363_326_733_505_337L, + 8_549_497_411_294_502L, + }; + + static final int[] PaxsonExponents = { + -342, + -824, + 237, + 72, + 99, + 726, + -456, + -57, + 376, + 377, + 93, + 710, + 709, + 117, + -1, + -707, + -381, + 721, + -828, + -345, + 202, + -473, + + 952, + 535, + 534, + -957, + -144, + 363, + -169, + -853, + -780, + -383, + -384, + -385, + -249, + -250, + -251, + 548, + 164, + 665, + 690, + 588, + 272, + -448, + }; + + /* + Random doubles over the whole range + */ + private static void testRandom(int randomCount, Random r) { + for (int i = 0; i < randomCount; ++i) { + toDec(longBitsToDouble(r.nextLong())); + } + } + + /* + Random doubles over the integer range [0, 2^52). + These are all exact doubles and exercise the fast path (except 0). + */ + private static void testRandomUnit(int randomCount, Random r) { + for (int i = 0; i < randomCount; ++i) { + toDec(r.nextLong() & (1L << P - 1)); + } + } + + /* + Random doubles over the range [0, 10^15) as "multiples" of 1e-3 + */ + private static void testRandomMilli(int randomCount, Random r) { + for (int i = 0; i < randomCount; ++i) { + toDec(r.nextLong() % 1_000_000_000_000_000_000L / 1e3); + } + } + + /* + Random doubles over the range [0, 10^15) as "multiples" of 1e-6 + */ + private static void testRandomMicro(int randomCount, Random r) { + for (int i = 0; i < randomCount; ++i) { + toDec((r.nextLong() & 0x7FFF_FFFF_FFFF_FFFFL) / 1e6); + } + } + + static void randomNumberTests(int randomCount, Random r) { + testRandom(randomCount, r); + testRandomUnit(randomCount, r); + testRandomMilli(randomCount, r); + testRandomMicro(randomCount, r); + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/io/schubfach/DoubleToDecimalTest.java b/src/test/java/com/fasterxml/jackson/core/io/schubfach/DoubleToDecimalTest.java new file mode 100644 index 0000000000..abf1c489ab --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/io/schubfach/DoubleToDecimalTest.java @@ -0,0 +1,130 @@ +package com.fasterxml.jackson.core.io.schubfach; + +import org.junit.jupiter.api.Test; + +import java.util.Random; + +import static com.fasterxml.jackson.core.io.schubfach.DoubleToDecimalChecker.*; +import static java.lang.Double.longBitsToDouble; +import static java.lang.StrictMath.scalb; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DoubleToDecimalTest { + @Test + void testExtremeValues() { + toDec(Double.NEGATIVE_INFINITY); + toDec(-Double.MAX_VALUE); + toDec(-Double.MIN_NORMAL); + toDec(-Double.MIN_VALUE); + toDec(-0.0); + toDec(0.0); + toDec(Double.MIN_VALUE); + toDec(Double.MIN_NORMAL); + toDec(Double.MAX_VALUE); + toDec(Double.POSITIVE_INFINITY); + toDec(Double.NaN); + + /* + Quiet NaNs have the most significant bit of the mantissa as 1, + while signaling NaNs have it as 0. + Exercise 4 combinations of quiet/signaling NaNs and + "positive/negative" NaNs + */ + toDec(longBitsToDouble(0x7FF8_0000_0000_0001L)); + toDec(longBitsToDouble(0x7FF0_0000_0000_0001L)); + toDec(longBitsToDouble(0xFFF8_0000_0000_0001L)); + toDec(longBitsToDouble(0xFFF0_0000_0000_0001L)); + + /* + All values treated specially by Schubfach + */ + for (int c = 1; c < C_TINY; ++c) { + toDec(c * Double.MIN_VALUE); + } + } + + /* + A few "powers of 10" are incorrectly rendered by the JDK. + The rendering is either too long or it is not the closest decimal. + */ + @Test + void testPowersOf10() { + for (int e = E_MIN; e <= E_MAX; ++e) { + toDec(Double.parseDouble("1e" + e)); + } + } + + /* + Many powers of 2 are incorrectly rendered by the JDK. + The rendering is either too long or it is not the closest decimal. + */ + @Test + void testPowersOf2() { + for (double v = Double.MIN_VALUE; v <= Double.MAX_VALUE; v *= 2) { + toDec(v); + } + } + + @Test + void testSomeAnomalies() { + for (String dec : Anomalies) { + toDec(Double.parseDouble(dec)); + } + } + + @Test + void testPaxson() { + for (int i = 0; i < PaxsonSignificands.length; ++i) { + toDec(scalb(PaxsonSignificands[i], PaxsonExponents[i])); + } + } + + /* + Tests all integers of the form yx_xxx_000_000_000_000_000, y != 0. + These are all exact doubles. + */ + @Test + void testLongs() { + for (int i = 10_000; i < 100_000; ++i) { + toDec(i * 1e15); + } + } + + /* + Tests all integers up to 1_000_000. + These are all exact doubles and exercise a fast path. + */ + @Test + void testInts() { + for (int i = 0; i <= 1_000_000; ++i) { + toDec(i); + } + } + + @Test + void testConstants() { + assertEquals(DoubleToDecimal.P, P, "P"); + assertTrue((long) (double) C_MIN == C_MIN, "C_MIN"); + assertTrue((long) (double) C_MAX == C_MAX, "C_MAX"); + assertEquals(Double.MIN_VALUE, MIN_VALUE, "MIN_VALUE"); + assertEquals(Double.MIN_NORMAL, MIN_NORMAL, "MIN_NORMAL"); + assertEquals(Double.MAX_VALUE, MAX_VALUE, "MAX_VALUE"); + + assertEquals(DoubleToDecimal.Q_MIN, Q_MIN, "Q_MIN"); + assertEquals(DoubleToDecimal.Q_MAX, Q_MAX, "Q_MAX"); + + assertEquals(DoubleToDecimal.K_MIN, K_MIN, "K_MIN"); + assertEquals(DoubleToDecimal.K_MAX, K_MAX, "K_MAX"); + assertEquals(DoubleToDecimal.H, H, "H"); + + assertEquals(DoubleToDecimal.E_MIN, E_MIN, "E_MIN"); + assertEquals(DoubleToDecimal.E_MAX, E_MAX, "E_MAX"); + assertEquals(DoubleToDecimal.C_TINY, C_TINY, "C_TINY"); + } + + @Test + void randomNumberTests() { + DoubleToDecimalChecker.randomNumberTests(1_000_000, new Random()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/io/schubfach/DoubleToStringTest.java b/src/test/java/com/fasterxml/jackson/core/io/schubfach/DoubleToStringTest.java index 686119a631..ac920d971d 100644 --- a/src/test/java/com/fasterxml/jackson/core/io/schubfach/DoubleToStringTest.java +++ b/src/test/java/com/fasterxml/jackson/core/io/schubfach/DoubleToStringTest.java @@ -63,7 +63,7 @@ public void regressionTest() { assertD2sEquals("4.708356024711512E18", 4.708356024711512E18); assertD2sEquals("9.409340012568248E18", 9.409340012568248E18); // This number naively requires 65 bit for the intermediate results if we reduce the lookup - // table by half. This checks that we don't loose any information in that case. + // table by half. This checks that we don't lose any information in that case. assertD2sEquals("1.8531501765868567E21", 1.8531501765868567E21); assertD2sEquals("-3.347727380279489E33", -3.347727380279489E33); // Discovered by Andriy Plokhotnyuk, see #29. diff --git a/src/test/java/com/fasterxml/jackson/core/io/schubfach/FloatToDecimalChecker.java b/src/test/java/com/fasterxml/jackson/core/io/schubfach/FloatToDecimalChecker.java new file mode 100644 index 0000000000..92107efcd2 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/io/schubfach/FloatToDecimalChecker.java @@ -0,0 +1,248 @@ +/* + * Copyright 2018-2020 Raffaello Giulietti + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.fasterxml.jackson.core.io.schubfach; + +import java.math.BigDecimal; +import java.util.Random; + +import static java.lang.Float.*; +import static java.lang.Integer.numberOfTrailingZeros; +import static java.lang.StrictMath.scalb; +import static com.fasterxml.jackson.core.io.schubfach.MathUtils.flog10pow2; + +public class FloatToDecimalChecker extends ToDecimalChecker { + + static final int P = + numberOfTrailingZeros(floatToRawIntBits(3)) + 2; + private static final int W = (SIZE - 1) - (P - 1); + static final int Q_MIN = (-1 << W - 1) - P + 3; + static final int Q_MAX = (1 << W - 1) - P; + static final int C_MIN = 1 << P - 1; + static final int C_MAX = (1 << P) - 1; + + static final int K_MIN = flog10pow2(Q_MIN); + static final int K_MAX = flog10pow2(Q_MAX); + static final int H = flog10pow2(P) + 2; + + static final float MIN_VALUE = scalb(1.0f, Q_MIN); + static final float MIN_NORMAL = scalb((float) C_MIN, Q_MIN); + static final float MAX_VALUE = scalb((float) C_MAX, Q_MAX); + + static final int E_MIN = e(MIN_VALUE); + static final int E_MAX = e(MAX_VALUE); + + static final long C_TINY = cTiny(Q_MIN, K_MIN); + + private float v; + private final int originalBits; + + private FloatToDecimalChecker(float v, String s) { + super(s); + this.v = v; + originalBits = floatToRawIntBits(v); + } + + @Override + BigDecimal toBigDecimal() { + return new BigDecimal(v); + } + + @Override + boolean recovers(BigDecimal b) { + return b.floatValue() == v; + } + + @Override + String hexBits() { + return String.format("0x%01X__%02X__%02X_%04X", + (originalBits >>> 31) & 0x1, + (originalBits >>> 23) & 0xFF, + (originalBits >>> 16) & 0x7F, + originalBits & 0xFFFF); + } + + @Override + boolean recovers(String s) { + return parseFloat(s) == v; + } + + @Override + int minExp() { + return E_MIN; + } + + @Override + int maxExp() { + return E_MAX; + } + + @Override + int maxLen10() { + return H; + } + + @Override + boolean isZero() { + return v == 0; + } + + @Override + boolean isInfinity() { + return v == POSITIVE_INFINITY; + } + + @Override + void negate() { + v = -v; + } + + @Override + boolean isNegative() { + return originalBits < 0; + } + + @Override + boolean isNaN() { + return Float.isNaN(v); + } + + static void toDec(float v) { +// String s = Float.toString(v); + String s = FloatToDecimal.toString(v); + new FloatToDecimalChecker(v, s).validate(); + } + + /* + There are tons of doubles that are rendered incorrectly by the JDK. + While the renderings correctly round back to the original value, + they are longer than needed or are not the closest decimal to the double. + Here are just a very few examples. + */ + static final String[] Anomalies = { + // JDK renders these longer than needed. + "1.1754944E-38", "2.2E-44", + "1.0E16", "2.0E16", "3.0E16", "5.0E16", "3.0E17", + "3.2E18", "3.7E18", "3.7E16", "3.72E17", + + // JDK does not render this as the closest. + "9.9E-44", + }; + + /* + Values are from + Paxson V, "A Program for Testing IEEE Decimal-Binary Conversion" + tables 16 and 17 + */ + static final float[] PaxsonSignificands = { + 12_676_506, + 15_445_013, + 13_734_123, + 12_428_269, + 12_676_506, + 15_334_037, + 11_518_287, + 12_584_953, + 15_961_084, + 14_915_817, + 10_845_484, + 16_431_059, + + 16_093_626, + 9_983_778, + 12_745_034, + 12_706_553, + 11_005_028, + 15_059_547, + 16_015_691, + 8_667_859, + 14_855_922, + 14_855_922, + 10_144_164, + 13_248_074, + }; + + static final int[] PaxsonExponents = { + -102, + -103, + 86, + -138, + -130, + -146, + -41, + -145, + -125, + -146, + -102, + -61, + + 69, + 25, + 104, + 72, + 45, + 71, + -99, + 56, + -82, + -83, + -110, + 95, + }; + + /* + Random floats over the whole range. + */ + private static void testRandom(int randomCount, Random r) { + for (int i = 0; i < randomCount; ++i) { + toDec(intBitsToFloat(r.nextInt())); + } + } + + /* + All, really all, 2^32 possible floats. Takes between 90 and 120 minutes. + */ + public static void testAll() { + // Avoid wrapping around Integer.MAX_VALUE + int bits = Integer.MIN_VALUE; + for (; bits < Integer.MAX_VALUE; ++bits) { + toDec(intBitsToFloat(bits)); + } + toDec(intBitsToFloat(bits)); + } + + /* + All positive 2^31 floats. + */ + public static void testPositive() { + // Avoid wrapping around Integer.MAX_VALUE + int bits = 0; + for (; bits < Integer.MAX_VALUE; ++bits) { + toDec(intBitsToFloat(bits)); + } + toDec(intBitsToFloat(bits)); + } + + public static void randomNumberTests(int randomCount, Random r) { + testRandom(randomCount, r); + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/io/schubfach/FloatToDecimalTest.java b/src/test/java/com/fasterxml/jackson/core/io/schubfach/FloatToDecimalTest.java new file mode 100644 index 0000000000..326ebc8cd0 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/io/schubfach/FloatToDecimalTest.java @@ -0,0 +1,122 @@ +package com.fasterxml.jackson.core.io.schubfach; + +import org.junit.jupiter.api.Test; + +import java.util.Random; + +import static com.fasterxml.jackson.core.io.schubfach.FloatToDecimalChecker.*; +import static java.lang.Float.intBitsToFloat; +import static java.lang.StrictMath.scalb; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class FloatToDecimalTest { + /* + MIN_NORMAL is incorrectly rendered by the JDK. + */ + @Test + void testExtremeValues() { + toDec(Float.NEGATIVE_INFINITY); + toDec(-Float.MAX_VALUE); + toDec(-Float.MIN_NORMAL); + toDec(-Float.MIN_VALUE); + toDec(-0.0f); + toDec(0.0f); + toDec(Float.MIN_VALUE); + toDec(Float.MIN_NORMAL); + toDec(Float.MAX_VALUE); + toDec(Float.POSITIVE_INFINITY); + toDec(Float.NaN); + + /* + Quiet NaNs have the most significant bit of the mantissa as 1, + while signaling NaNs have it as 0. + Exercise 4 combinations of quiet/signaling NaNs and + "positive/negative" NaNs. + */ + toDec(intBitsToFloat(0x7FC0_0001)); + toDec(intBitsToFloat(0x7F80_0001)); + toDec(intBitsToFloat(0xFFC0_0001)); + toDec(intBitsToFloat(0xFF80_0001)); + + /* + All values treated specially by Schubfach + */ + for (int c = 1; c < C_TINY; ++c) { + toDec(c * Float.MIN_VALUE); + } + } + + /* + Some "powers of 10" are incorrectly rendered by the JDK. + The rendering is either too long or it is not the closest decimal. + */ + @Test + void testPowersOf10() { + for (int e = E_MIN; e <= E_MAX; ++e) { + toDec(Float.parseFloat("1e" + e)); + } + } + + /* + Many powers of 2 are incorrectly rendered by the JDK. + The rendering is either too long or it is not the closest decimal. + */ + @Test + void testPowersOf2() { + for (float v = Float.MIN_VALUE; v <= Float.MAX_VALUE; v *= 2) { + toDec(v); + } + } + + @Test + void testConstants() { + assertEquals(FloatToDecimal.P, P, "P"); + assertTrue((long) (float) C_MIN == C_MIN, "C_MIN"); + assertTrue((long) (float) C_MAX == C_MAX, "C_MAX"); + assertEquals(Float.MIN_VALUE, MIN_VALUE, "MIN_VALUE"); + assertEquals(Float.MIN_NORMAL, MIN_NORMAL, "MIN_NORMAL"); + assertEquals(Float.MAX_VALUE, MAX_VALUE, "MAX_VALUE"); + + assertEquals(FloatToDecimal.Q_MIN, Q_MIN, "Q_MIN"); + assertEquals(FloatToDecimal.Q_MAX, Q_MAX, "Q_MAX"); + + assertEquals(FloatToDecimal.K_MIN, K_MIN, "K_MIN"); + assertEquals(FloatToDecimal.K_MAX, K_MAX, "K_MAX"); + assertEquals(FloatToDecimal.H, H, "H"); + + assertEquals(FloatToDecimal.E_MIN, E_MIN, "E_MIN"); + assertEquals(FloatToDecimal.E_MAX, E_MAX, "E_MAX"); + assertEquals(FloatToDecimal.C_TINY, C_TINY, "C_TINY"); + } + + @Test + void testSomeAnomalies() { + for (String dec : Anomalies) { + toDec(Float.parseFloat(dec)); + } + } + + @Test + void testPaxson() { + for (int i = 0; i < PaxsonSignificands.length; ++i) { + toDec(scalb(PaxsonSignificands[i], PaxsonExponents[i])); + } + } + + /* + Tests all positive integers below 2^23. + These are all exact floats and exercise the fast path. + */ + @Test + void testInts() { + for (int i = 1; i < 1 << P - 1; ++i) { + toDec(i); + } + } + + @Test + void randomNumberTests() { + FloatToDecimalChecker.randomNumberTests(1_000_000, new Random()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/io/schubfach/MathUtilsTest.java b/src/test/java/com/fasterxml/jackson/core/io/schubfach/MathUtilsTest.java new file mode 100644 index 0000000000..95de7c8fc7 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/io/schubfach/MathUtilsTest.java @@ -0,0 +1,470 @@ +/* + * Copyright 2018-2020 Raffaello Giulietti + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.fasterxml.jackson.core.io.schubfach; + +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; + +import static java.lang.Double.*; +import static java.lang.Long.numberOfTrailingZeros; +import static java.lang.StrictMath.scalb; +import static java.math.BigInteger.*; +import static com.fasterxml.jackson.core.io.schubfach.MathUtils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class MathUtilsTest { + + private static final BigInteger THREE = valueOf(3); + + // binary constants + private static final int P = + numberOfTrailingZeros(doubleToRawLongBits(3)) + 2; + private static final int W = (SIZE - 1) - (P - 1); + private static final int Q_MIN = (-1 << W - 1) - P + 3; + private static final int Q_MAX = (1 << W - 1) - P; + private static final long C_MIN = 1L << P - 1; + private static final long C_MAX = (1L << P) - 1; + + // decimal constants + private static final int K_MIN = flog10pow2(Q_MIN); + private static final int K_MAX = flog10pow2(Q_MAX); + private static final int H = flog10pow2(P) + 2; + + /* + Let + 10^(-k) = beta 2^r + for the unique integer r and real beta meeting + 2^125 <= beta < 2^126 + Further, let g = g1 2^63 + g0. + Checks that: + 2^62 <= g1 < 2^63, + 0 <= g0 < 2^63, + g - 1 <= beta < g, (that is, g = floor(beta) + 1) + The last predicate, after multiplying by 2^r, is equivalent to + (g - 1) 2^r <= 10^(-k) < g 2^r + This is the predicate that will be checked in various forms. + */ + private static void testG(int k, long g1, long g0) { + // 2^62 <= g1 < 2^63, 0 <= g0 < 2^63 + assertTrue(g1 << 1 < 0 && g1 >= 0 && g0 >= 0, "g"); + + BigInteger g = valueOf(g1).shiftLeft(63).or(valueOf(g0)); + // double check that 2^125 <= g < 2^126 + assertTrue(g.signum() > 0 && g.bitLength() == 126, "g"); + + // see javadoc of MathUtils.g1(int) + int r = flog2pow10(-k) - 125; + + /* + The predicate + (g - 1) 2^r <= 10^(-k) < g 2^r + is equivalent to + g - 1 <= 10^(-k) 2^(-r) < g + When + k <= 0 & r < 0 + all numerical subexpressions are integer-valued. This is the same as + g - 1 = 10^(-k) 2^(-r) + */ + if (k <= 0 && r < 0) { + assertEquals( + 0, + g.subtract(ONE).compareTo(TEN.pow(-k).shiftLeft(-r)), + "g"); + return; + } + + /* + The predicate + (g - 1) 2^r <= 10^(-k) < g 2^r + is equivalent to + g 10^k - 10^k <= 2^(-r) < g 10^k + When + k > 0 & r < 0 + all numerical subexpressions are integer-valued. + */ + if (k > 0 && r < 0) { + BigInteger pow5 = TEN.pow(k); + BigInteger mhs = ONE.shiftLeft(-r); + BigInteger rhs = g.multiply(pow5); + assertTrue(rhs.subtract(pow5).compareTo(mhs) <= 0 + && mhs.compareTo(rhs) < 0, + "g"); + return; + } + + /* + Finally, when + k <= 0 & r >= 0 + the predicate + (g - 1) 2^r <= 10^(-k) < g 2^r + can be used straightforwardly as all numerical subexpressions are + already integer-valued. + */ + if (k <= 0) { + BigInteger mhs = TEN.pow(-k); + assertTrue(g.subtract(ONE).shiftLeft(r).compareTo(mhs) <= 0 && + mhs.compareTo(g.shiftLeft(r)) < 0, + "g"); + return; + } + + /* + For combinatorial reasons, the only remaining case is + k > 0 & r >= 0 + which, however, cannot arise. Indeed, the predicate + (g - 1) 2^r <= 10^(-k) < g 2^r + has a positive integer left-hand side and a middle side < 1, + which cannot hold. + */ + fail("g"); + } + + /* + Verifies the soundness of the values returned by g1() and g0(). + */ + @Test + void testG() { + for (int k = MathUtils.K_MIN; k <= MathUtils.K_MAX; ++k) { + testG(k, g1(k), g0(k)); + } + } + + /* + Let + k = floor(log10(3/4 2^e)) + The method verifies that + k = flog10threeQuartersPow2(e), Q_MIN <= e <= Q_MAX + This range covers all binary exponents of doubles and floats. + + The first equation above is equivalent to + 10^k <= 3 2^(e-2) < 10^(k+1) + Equality never holds. Henceforth, the predicate to check is + 10^k < 3 2^(e-2) < 10^(k+1) + This will be transformed in various ways for checking purposes. + + For integer n > 0, let further + b = len2(n) + denote its length in bits. This means exactly the same as + 2^(b-1) <= n < 2^b + */ + @Test + void testFlog10threeQuartersPow2() { + // First check the case e = 1 + assertEquals(0, flog10threeQuartersPow2(1), + "flog10threeQuartersPow2"); + + /* + Now check the range Q_MIN <= e <= 0. + By rewriting, the predicate to check is equivalent to + 3 10^(-k-1) < 2^(2-e) < 3 10^(-k) + As e <= 0, it follows that 2^(2-e) >= 4 and the right inequality + implies k < 0, so the powers of 10 are integers. + + The left inequality is equivalent to + len2(3 10^(-k-1)) <= 2 - e + and the right inequality to + 2 - e < len2(3 10^(-k)) + The original predicate is therefore equivalent to + len2(3 10^(-k-1)) <= 2 - e < len2(3 10^(-k)) + + Starting with e = 0 and decrementing until the lower bound, the code + keeps track of the two powers of 10 to avoid recomputing them. + This is easy because at each iteration k changes at most by 1. A simple + multiplication by 10 computes the next power of 10 when needed. + */ + int e = 0; + int k0 = flog10threeQuartersPow2(e); + assertTrue(k0 < 0, "flog10threeQuartersPow2"); + BigInteger l = THREE.multiply(TEN.pow(-k0 - 1)); + BigInteger u = l.multiply(TEN); + for (;;) { + assertTrue(l.bitLength() <= 2 - e & 2 - e < u.bitLength(), + "flog10threeQuartersPow2"); + --e; + if (e < Q_MIN) { + break; + } + int kp = flog10threeQuartersPow2(e); + assertTrue(kp <= k0, "flog10threeQuartersPow2"); + if (kp < k0) { + // k changes at most by 1 at each iteration, hence: + assertEquals(1, k0 - kp, "flog10threeQuartersPow2"); + k0 = kp; + l = u; + u = u.multiply(TEN); + } + } + + /* + Finally, check the range 2 <= e <= Q_MAX. + In predicate + 10^k < 3 2^(e-2) < 10^(k+1) + the right inequality shows that k >= 0 as soon as e >= 2. + It is equivalent to + 10^k / 3 < 2^(e-2) < 10^(k+1) / 3 + Both the powers of 10 and the powers of 2 are integers. + The left inequality is therefore equivalent to + floor(10^k / 3) < 2^(e-2) + and thus to + len2(floor(10^k / 3)) <= e - 2 + while the right inequality is equivalent to + 2^(e-2) <= floor(10^(k+1) / 3) + and hence to + e - 2 < len2(floor(10^(k+1) / 3)) + These are summarized as + len2(floor(10^k / 3)) <= e - 2 < len2(floor(10^(k+1) / 3)) + */ + e = 2; + k0 = flog10threeQuartersPow2(e); + assertTrue(k0 >= 0, "flog10threeQuartersPow2"); + BigInteger l10 = TEN.pow(k0); + BigInteger u10 = l10.multiply(TEN); + l = l10.divide(THREE); + u = u10.divide(THREE); + for (;;) { + assertTrue(l.bitLength() <= e - 2 & e - 2 < u.bitLength(), + "flog10threeQuartersPow2"); + ++e; + if (e > Q_MAX) { + break; + } + int kp = flog10threeQuartersPow2(e); + assertTrue(kp >= k0, "flog10threeQuartersPow2"); + if (kp > k0) { + // k changes at most by 1 at each iteration, hence: + assertEquals(1, kp - k0, "flog10threeQuartersPow2"); + k0 = kp; + u10 = u10.multiply(TEN); + l = u; + u = u10.divide(THREE); + } + } + } + + /* + Let + k = floor(log10(2^e)) + The method verifies that + k = flog10pow2(e), Q_MIN <= e <= Q_MAX + This range covers all binary exponents of doubles and floats. + + The first equation above is equivalent to + 10^k <= 2^e < 10^(k+1) + Equality holds iff e = k = 0. + Henceforth, the predicates to check are equivalent to + k = 0, if e = 0 + 10^k < 2^e < 10^(k+1), otherwise + The latter will be transformed in various ways for checking purposes. + + For integer n > 0, let further + b = len2(n) + denote its length in bits. This means exactly the same as + 2^(b-1) <= n < 2^b + */ + @Test + void testFlog10pow2() { + // First check the case e = 0 + assertEquals(0, flog10pow2(0), "flog10pow2"); + + /* + Now check the range F * Q_MIN <= e < 0. + By inverting all quantities, the predicate to check is equivalent to + 10^(-k-1) < 2^(-e) < 10^(-k) + As e < 0, it follows that 2^(-e) >= 2 and the right inequality + implies k < 0. + The left inequality means exactly the same as + len2(10^(-k-1)) <= -e + Similarly, the right inequality is equivalent to + -e < len2(10^(-k)) + The original predicate is therefore equivalent to + len2(10^(-k-1)) <= -e < len2(10^(-k)) + The powers of 10 are integers because k < 0. + + Starting with e = -1 and decrementing towards the lower bound, the code + keeps track of the two powers of 10 so as to avoid recomputing them. + This is easy because at each iteration k changes at most by 1. A simple + multiplication by 10 computes the next power of 10 when needed. + */ + int e = -1; + int k = flog10pow2(e); + assertTrue(k < 0, "flog10pow2"); + BigInteger l = TEN.pow(-k - 1); + BigInteger u = l.multiply(TEN); + for (;;) { + assertTrue(l.bitLength() <= -e & -e < u.bitLength(), + "flog10pow2"); + --e; + if (e < Q_MIN) { + break; + } + int kp = flog10pow2(e); + assertTrue(kp <= k, "flog10pow2"); + if (kp < k) { + // k changes at most by 1 at each iteration, hence: + assertEquals(1, k - kp, "flog10pow2"); + k = kp; + l = u; + u = u.multiply(TEN); + } + } + + /* + Finally, in a similar vein, check the range 0 <= e <= Q_MAX. + In predicate + 10^k < 2^e < 10^(k+1) + the right inequality shows that k >= 0. + The left inequality means the same as + len2(10^k) <= e + and the right inequality holds iff + e < len2(10^(k+1)) + The original predicate is thus equivalent to + len2(10^k) <= e < len2(10^(k+1)) + As k >= 0, the powers of 10 are integers. + */ + e = 1; + k = flog10pow2(e); + assertTrue(k >= 0, "flog10pow2"); + l = TEN.pow(k); + u = l.multiply(TEN); + for (;;) { + assertTrue(l.bitLength() <= e & e < u.bitLength(), + "flog10pow2"); + ++e; + if (e > Q_MAX) { + break; + } + int kp = flog10pow2(e); + assertTrue(kp >= k, "flog10pow2"); + if (kp > k) { + // k changes at most by 1 at each iteration, hence: + assertEquals(1, kp - k, "flog10pow2"); + k = kp; + l = u; + u = u.multiply(TEN); + } + } + } + + /* + Let + k = floor(log2(10^e)) + The method verifies that + k = flog2pow10(e), -K_MAX <= e <= -K_MIN + This range covers all decimal exponents of doubles and floats. + + The first equation above is equivalent to + 2^k <= 10^e < 2^(k+1) + Equality holds iff e = 0, implying k = 0. + Henceforth, the equivalent predicates to check are + k = 0, if e = 0 + 2^k < 10^e < 2^(k+1), otherwise + The latter will be transformed in various ways for checking purposes. + + For integer n > 0, let further + b = len2(n) + denote its length in bits. This means exactly the same as + 2^(b-1) <= n < 2^b + */ + @Test + void testFlog2pow10() { + // First check the case e = 0 + assertEquals(0, flog2pow10(0), "flog2pow10"); + + /* + Now check the range K_MIN <= e < 0. + By inverting all quantities, the predicate to check is equivalent to + 2^(-k-1) < 10^(-e) < 2^(-k) + As e < 0, this leads to 10^(-e) >= 10 and the right inequality implies + k <= -4. + The above means the same as + len2(10^(-e)) = -k + The powers of 10 are integer values since e < 0. + */ + int e = -1; + int k0 = flog2pow10(e); + assertTrue(k0 <= -4, "flog2pow10"); + BigInteger l = TEN; + for (;;) { + assertEquals(-k0, l.bitLength(), "flog2pow10"); + --e; + if (e < -K_MAX) { + break; + } + k0 = flog2pow10(e); + l = l.multiply(TEN); + } + + /* + Finally check the range 0 < e <= K_MAX. + From the predicate + 2^k < 10^e < 2^(k+1) + as e > 0, it follows that 10^e >= 10 and the right inequality implies + k >= 3. + The above means the same as + len2(10^e) = k + 1 + The powers of 10 are all integer valued, as e > 0. + */ + e = 1; + k0 = flog2pow10(e); + assertTrue(k0 >= 3, "flog2pow10"); + l = TEN; + for (;;) { + assertEquals(k0 + 1, l.bitLength(), "flog2pow10"); + ++e; + if (e > -K_MIN) { + break; + } + k0 = flog2pow10(e); + l = l.multiply(TEN); + } + } + + @Test + void testBinaryConstants() { + assertTrue((long) (double) C_MIN == C_MIN, "C_MIN"); + assertTrue((long) (double) C_MAX == C_MAX, "C_MAX"); + assertEquals(MIN_VALUE, scalb(1.0, Q_MIN), "MIN_VALUE"); + assertEquals(MIN_NORMAL, scalb((double) C_MIN, Q_MIN), "MIN_NORMAL"); + assertEquals(MAX_VALUE, scalb((double) C_MAX, Q_MAX), "MAX_VALUE"); + } + + @Test + void testDecimalConstants() { + assertEquals(MathUtils.K_MIN, K_MIN, "K_MIN"); + assertEquals(MathUtils.K_MAX, K_MAX, "K_MAX"); + assertEquals(MathUtils.H, H, "H"); + } + + @Test + void testPow10() { + int e = 0; + long pow = 1; + for (; e <= H; e += 1, pow *= 10) { + assertEquals(pow10(e), pow, "pow10"); + } + } + +} diff --git a/src/test/java/com/fasterxml/jackson/core/io/schubfach/ToDecimalChecker.java b/src/test/java/com/fasterxml/jackson/core/io/schubfach/ToDecimalChecker.java new file mode 100644 index 0000000000..0852a81a45 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/io/schubfach/ToDecimalChecker.java @@ -0,0 +1,404 @@ +/* + * Copyright 2018-2020 Raffaello Giulietti + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.fasterxml.jackson.core.io.schubfach; + +import java.io.IOException; +import java.io.StringReader; +import java.math.BigDecimal; +import java.math.BigInteger; + +import static java.math.BigInteger.*; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* +A checker for the Javadoc specification. +It just relies on straightforward use of (expensive) BigDecimal arithmetic, +not optimized at all. + */ +abstract class ToDecimalChecker { + + // The string to check + private final String s; + + // The decimal parsed from s is c 10^q + private long c; + private int q; + + // The number of digits parsed from s: 10^(len10-1) <= c < 10^len10 + private int len10; + + ToDecimalChecker(String s) { + this.s = s; + } + + /* + Returns e be such that 10^(e-1) <= v < 10^e. + */ + static int e(double v) { + // log10(v) + 1 is a first good approximation of e + int e = (int) Math.floor(Math.log10(v)) + 1; + + // Full precision search for e such that 10^(e-1) <= c 2^q < 10^e. + BigDecimal vp = new BigDecimal(v); + BigDecimal low = new BigDecimal(BigInteger.ONE, -(e - 1)); + while (low.compareTo(vp) > 0) { + e -= 1; + low = new BigDecimal(BigInteger.ONE, -(e - 1)); + } + BigDecimal high = new BigDecimal(BigInteger.ONE, -e); + while (vp.compareTo(high) >= 0) { + e += 1; + high = new BigDecimal(BigInteger.ONE, -e); + } + return e; + } + + static long cTiny(int qMin, int kMin) { + BigInteger[] qr = ONE.shiftLeft(-qMin) + .divideAndRemainder(TEN.pow(-(kMin + 1))); + BigInteger cTiny = qr[1].signum() > 0 ? qr[0].add(ONE) : qr[0]; + assertTrue(cTiny.bitLength() < Long.SIZE, "C_TINY"); + return cTiny.longValue(); + } + + void validate() { + String msg = "toString applied to the bits " + + hexBits() + + " returns " + + "\"" + s + "\"" + + ", which is not correct according to the specification."; + assertTrue(isOK(), msg); + } + + /* + Returns whether s syntactically meets the expected output of + toString. It is restricted to finite positive outputs. + It is an unusually long method but rather straightforward, too. + Many conditionals could be merged, but KISS here. + */ + private boolean parse(String t) { + try { + // first determine interesting boundaries in the string + StringReader r = new StringReader(t); + int ch = r.read(); + + int i = 0; + while (ch == '0') { + ++i; + ch = r.read(); + } + // i is just after zeroes starting the integer + + int p = i; + while ('0' <= ch && ch <= '9') { + c = 10 * c + (ch - '0'); + if (c < 0) { + return false; + } + ++len10; + ++p; + ch = r.read(); + } + // p is just after digits ending the integer + + int fz = p; + if (ch == '.') { + ++fz; + ch = r.read(); + } + // fz is just after a decimal '.' + + int f = fz; + while (ch == '0') { + c = 10 * c + (ch - '0'); + if (c < 0) { + return false; + } + ++len10; + ++f; + ch = r.read(); + } + // f is just after zeroes starting the fraction + + if (c == 0) { + len10 = 0; + } + int x = f; + while ('0' <= ch && ch <= '9') { + c = 10 * c + (ch - '0'); + if (c < 0) { + return false; + } + ++len10; + ++x; + ch = r.read(); + } + // x is just after digits ending the fraction + + int g = x; + if (ch == 'E') { + ++g; + ch = r.read(); + } + // g is just after an exponent indicator 'E' + + int ez = g; + if (ch == '-') { + ++ez; + ch = r.read(); + } + // ez is just after a '-' sign in the exponent + + int e = ez; + while (ch == '0') { + ++e; + ch = r.read(); + } + // e is just after zeroes starting the exponent + + int z = e; + while ('0' <= ch && ch <= '9') { + q = 10 * q + (ch - '0'); + if (q < 0) { + return false; + } + ++z; + ch = r.read(); + } + // z is just after digits ending the exponent + + // No other char after the number + if (z != t.length()) { + return false; + } + + // The integer must be present + if (p == 0) { + return false; + } + + // The decimal '.' must be present + if (fz == p) { + return false; + } + + // The fraction must be present + if (x == fz) { + return false; + } + + // The fraction is not 0 or it consists of exactly one 0 + if (f == x && f - fz > 1) { + return false; + } + + // Plain notation, no exponent + if (x == z) { + // At most one 0 starting the integer + if (i > 1) { + return false; + } + + // If the integer is 0, at most 2 zeroes start the fraction + if (i == 1 && f - fz > 2) { + return false; + } + + // The integer cannot have more than 7 digits + if (p > 7) { + return false; + } + + q = fz - x; + + // OK for plain notation + return true; + } + + // Computerized scientific notation + + // The integer has exactly one nonzero digit + if (i != 0 || p != 1) { + return false; + } + + // + // There must be an exponent indicator + if (x == g) { + return false; + } + + // There must be an exponent + if (ez == z) { + return false; + } + + // The exponent must not start with zeroes + if (ez != e) { + return false; + } + + if (g != ez) { + q = -q; + } + + // The exponent must not lie in [-3, 7) + if (-3 <= q && q < 7) { + return false; + } + + q += fz - x; + + // OK for computerized scientific notation + return true; + } catch (IOException ex) { + // An IOException on a StringReader??? Please... + return false; + } + } + + private boolean isOK() { + if (isNaN()) { + return s.equals("NaN"); + } + String t = s; + if (isNegative()) { + if (s.isEmpty() || s.charAt(0) != '-') { + return false; + } + negate(); + t = s.substring(1); + } + if (isInfinity()) { + return t.equals("Infinity"); + } + if (isZero()) { + return t.equals("0.0"); + } + if (!parse(t)) { + return false; + } + if (len10 < 2) { + c *= 10; + q -= 1; + len10 += 1; + } + if (2 > len10 || len10 > maxLen10()) { + return false; + } + + // The exponent is bounded + if (minExp() > q + len10 || q + len10 > maxExp()) { + return false; + } + + // s must recover v + try { + if (!recovers(t)) { + return false; + } + } catch (NumberFormatException e) { + return false; + } + + // Get rid of trailing zeroes, still ensuring at least 2 digits + while (len10 > 2 && c % 10 == 0) { + c /= 10; + q += 1; + len10 -= 1; + } + + if (len10 > 2) { + // Try with a shorter number less than v... + if (recovers(BigDecimal.valueOf(c / 10, -q - 1))) { + return false; + } + + // ... and with a shorter number greater than v + if (recovers(BigDecimal.valueOf(c / 10 + 1, -q - 1))) { + return false; + } + } + + // Try with the decimal predecessor... + BigDecimal dp = c == 10 ? + BigDecimal.valueOf(99, -q + 1) : + BigDecimal.valueOf(c - 1, -q); + if (recovers(dp)) { + BigDecimal bv = toBigDecimal(); + BigDecimal deltav = bv.subtract(BigDecimal.valueOf(c, -q)); + if (deltav.signum() >= 0) { + return true; + } + BigDecimal delta = dp.subtract(bv); + if (delta.signum() >= 0) { + return false; + } + int cmp = deltav.compareTo(delta); + return cmp > 0 || cmp == 0 && (c & 0x1) == 0; + } + + // ... and with the decimal successor + BigDecimal ds = BigDecimal.valueOf(c + 1, -q); + if (recovers(ds)) { + BigDecimal bv = toBigDecimal(); + BigDecimal deltav = bv.subtract(BigDecimal.valueOf(c, -q)); + if (deltav.signum() <= 0) { + return true; + } + BigDecimal delta = ds.subtract(bv); + if (delta.signum() <= 0) { + return false; + } + int cmp = deltav.compareTo(delta); + return cmp < 0 || cmp == 0 && (c & 0x1) == 0; + } + + return true; + } + + abstract BigDecimal toBigDecimal(); + + abstract boolean recovers(BigDecimal b); + + abstract boolean recovers(String s); + + abstract String hexBits(); + + abstract int minExp(); + + abstract int maxExp(); + + abstract int maxLen10(); + + abstract boolean isZero(); + + abstract boolean isInfinity(); + + abstract void negate(); + + abstract boolean isNegative(); + + abstract boolean isNaN(); + +}