NumberOutputFloatDoubleTest.java

package tools.jackson.core.unittest.io;

import java.util.Random;

import org.junit.jupiter.api.Test;

import tools.jackson.core.io.NumberOutput;
import tools.jackson.core.io.xjb.XJBWriter;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Tests for {@link NumberOutput#outputFloat(float, char[], int)} and
 * {@link NumberOutput#outputDouble(double, char[], int)}.
 *
 * Verifies that the NumberOutput char[] methods produce the same output as
 * XJBWriter.toString, and that the returned position is correct.
 */
public class NumberOutputFloatDoubleTest
{
    // ------------------------------------------------------------------
    // Float
    // ------------------------------------------------------------------

    @Test
    public void floatSimpleCases() {
        assertFloatOutputMatchesToString(0f);
        assertFloatOutputMatchesToString(-0f);
        assertFloatOutputMatchesToString(1f);
        assertFloatOutputMatchesToString(-1f);
        // NaN and Infinity are now handled directly
        assertFloatOutputMatchesToString(Float.NaN);
        assertFloatOutputMatchesToString(Float.POSITIVE_INFINITY);
        assertFloatOutputMatchesToString(Float.NEGATIVE_INFINITY);
    }

    @Test
    public void floatBoundaryConditions() {
        assertFloatOutputMatchesToString(1.0E7f);
        assertFloatOutputMatchesToString(9999999.0f);
        assertFloatOutputMatchesToString(0.001f);
        assertFloatOutputMatchesToString(0.0009999999f);
    }

    @Test
    public void floatMinMax() {
        assertFloatOutputMatchesToString(Float.MAX_VALUE);
        assertFloatOutputMatchesToString(Float.MIN_VALUE);
        assertFloatOutputMatchesToString(Float.MIN_NORMAL);
        assertFloatOutputMatchesToString(-Float.MAX_VALUE);
        assertFloatOutputMatchesToString(-Float.MIN_VALUE);
        // longest output
        assertFloatOutputMatchesToString(-Float.MIN_NORMAL);
    }

    @Test
    public void floatSubnormals() {
        assertFloatOutputMatchesToString(Float.intBitsToFloat(0x00800000));
        assertFloatOutputMatchesToString(Float.intBitsToFloat(0x00000001));
        assertFloatOutputMatchesToString(Float.intBitsToFloat(0x00000002));
        assertFloatOutputMatchesToString(Float.intBitsToFloat(0x007fffff));
    }

    @Test
    public void floatRegression() {
        assertFloatOutputMatchesToString(4.7223665E21f);
        assertFloatOutputMatchesToString(8388608.0f);
        assertFloatOutputMatchesToString(1.6777216E7f);
        assertFloatOutputMatchesToString(3.3554436E7f);
        assertFloatOutputMatchesToString(6.7131496E7f);
        assertFloatOutputMatchesToString(1.9310392E-38f);
        assertFloatOutputMatchesToString(-2.47E-43f);
        assertFloatOutputMatchesToString(1.993244E-38f);
        assertFloatOutputMatchesToString(4103.9004f);
        assertFloatOutputMatchesToString(5.3399997E9f);
        assertFloatOutputMatchesToString(6.0898E-39f);
        assertFloatOutputMatchesToString(0.0010310042f);
        assertFloatOutputMatchesToString(2.8823261E17f);
        assertFloatOutputMatchesToString(7.038531E-26f);
        assertFloatOutputMatchesToString(9.2234038E17f);
        assertFloatOutputMatchesToString(6.7108872E7f);
        assertFloatOutputMatchesToString(1.0E-44f);
        assertFloatOutputMatchesToString(2.816025E14f);
        assertFloatOutputMatchesToString(9.223372E18f);
        assertFloatOutputMatchesToString(1.5846085E29f);
        assertFloatOutputMatchesToString(1.1811161E19f);
        assertFloatOutputMatchesToString(5.368709E18f);
        assertFloatOutputMatchesToString(4.6143165E18f);
        assertFloatOutputMatchesToString(0.007812537f);
        assertFloatOutputMatchesToString(1.4E-45f);
        assertFloatOutputMatchesToString(1.18697724E20f);
        assertFloatOutputMatchesToString(1.00014165E-36f);
        assertFloatOutputMatchesToString(200f);
        assertFloatOutputMatchesToString(3.3554432E7f);
    }

    @Test
    public void floatRounding() {
        assertFloatOutputMatchesToString(3.3554448E7f);
        assertFloatOutputMatchesToString(8.999999E9f);
        assertFloatOutputMatchesToString(3.4366717E10f);
        assertFloatOutputMatchesToString(0.33007812f);
    }

    @Test
    public void floatLooksLikePow5() {
        assertFloatOutputMatchesToString(Float.intBitsToFloat(0x5D1502F9));
        assertFloatOutputMatchesToString(Float.intBitsToFloat(0x5D9502F9));
        assertFloatOutputMatchesToString(Float.intBitsToFloat(0x5E1502F9));
    }

    @Test
    public void floatWithOffset() {
        float[] values = {0f, -0f, 1f, -1f, 3.14f, -3.14f, 1.0E7f, 0.001f,
                Float.MAX_VALUE, Float.MIN_VALUE};
        for (float f : values) {
            for (int offset = 0; offset <= 4; offset++) {
                assertFloatOutputWithOffset(f, offset);
            }
        }
        // NaN and Infinity are now handled directly
        assertFloatOutputWithOffset(Float.NaN, 4);
        assertFloatOutputWithOffset(Float.POSITIVE_INFINITY, 4);
        assertFloatOutputWithOffset(Float.NEGATIVE_INFINITY, 4);
    }

    @Test
    public void floatRandom() {
        // Round-trip correctness is covered by XJBRoundTripTest; here just verify char[] == toString
        Random r = new Random(42);
        for (int i = 0; i < 100_000; i++) {
            assertFloatOutputMatchesToString(Float.intBitsToFloat(r.nextInt()));
        }
    }

    // ------------------------------------------------------------------
    // Double
    // ------------------------------------------------------------------

    @Test
    public void doubleSimpleCases() {
        assertDoubleOutputMatchesToString(0d);
        assertDoubleOutputMatchesToString(-0d);
        assertDoubleOutputMatchesToString(1d);
        assertDoubleOutputMatchesToString(-1d);
        // NaN and Infinity are now handled directly
        assertDoubleOutputMatchesToString(Double.NaN);
        assertDoubleOutputMatchesToString(Double.POSITIVE_INFINITY);
        assertDoubleOutputMatchesToString(Double.NEGATIVE_INFINITY);
    }

    @Test
    public void doubleBoundaryConditions() {
        assertDoubleOutputMatchesToString(1.0E7d);
        assertDoubleOutputMatchesToString(9999999.999999998d);
        assertDoubleOutputMatchesToString(0.001d);
        assertDoubleOutputMatchesToString(0.0009999999999999998d);
    }

    @Test
    public void doubleMinMax() {
        assertDoubleOutputMatchesToString(Double.MAX_VALUE);
        assertDoubleOutputMatchesToString(Double.MIN_VALUE);
        assertDoubleOutputMatchesToString(Double.MIN_NORMAL);
        assertDoubleOutputMatchesToString(-Double.MAX_VALUE);
        assertDoubleOutputMatchesToString(-Double.MIN_VALUE);
        // longest output
        assertDoubleOutputMatchesToString(-Double.MIN_NORMAL);
    }

    @Test
    public void doubleSubnormals() {
        assertDoubleOutputMatchesToString(Double.longBitsToDouble(0x0010000000000000L));
        assertDoubleOutputMatchesToString(Double.longBitsToDouble(1L));
        assertDoubleOutputMatchesToString(Double.longBitsToDouble(2L));
        assertDoubleOutputMatchesToString(Double.longBitsToDouble(0x000FFFFFFFFFFFFFL));
    }

    @Test
    public void doubleRegression() {
        assertDoubleOutputMatchesToString(4.940656E-318d);
        assertDoubleOutputMatchesToString(1.18575755E-316d);
        assertDoubleOutputMatchesToString(2.989102097996E-312d);
        assertDoubleOutputMatchesToString(9.0608011534336E15d);
        assertDoubleOutputMatchesToString(4.708356024711512E18);
        assertDoubleOutputMatchesToString(9.409340012568248E18);
        assertDoubleOutputMatchesToString(1.8531501765868567E21);
        assertDoubleOutputMatchesToString(-3.347727380279489E33);
        assertDoubleOutputMatchesToString(1.9430376160308388E16);
        assertDoubleOutputMatchesToString(-6.9741824662760956E19);
        assertDoubleOutputMatchesToString(4.3816050601147837E18);
    }

    @Test
    public void doubleRounding() {
        assertDoubleOutputMatchesToString(-2.109808898695963E16);
    }

    @Test
    public void doubleWithOffset() {
        double[] values = {0d, -0d, 1d, -1d, 3.14d, -3.14d, 1.0E7d, 0.001d,
                Double.MAX_VALUE, Double.MIN_VALUE, 4.9E-324, 2.2250738585072014E-308};
        for (double d : values) {
            for (int offset = 0; offset <= 4; offset++) {
                assertDoubleOutputWithOffset(d, offset);
            }
        }
        // NaN and Infinity are now handled directly
        assertDoubleOutputWithOffset(Double.NaN, 4);
        assertDoubleOutputWithOffset(Double.POSITIVE_INFINITY, 4);
        assertDoubleOutputWithOffset(Double.NEGATIVE_INFINITY, 4);
    }

    @Test
    public void doubleRandom() {
        // Round-trip correctness is covered by XJBRoundTripTest; here just verify char[] == toString
        Random r = new Random(42);
        for (int i = 0; i < 100_000; i++) {
            assertDoubleOutputMatchesToString(Double.longBitsToDouble(r.nextLong()));
            assertDoubleOutputMatchesToString((r.nextDouble() - 0.5) * Math.pow(10, r.nextInt(60) - 30));
        }
    }

    // ------------------------------------------------------------------
    // Constants
    // ------------------------------------------------------------------

    @Test
    public void maxFloatCharsConstant() {
        // Verify MAX_FLOAT_CHARS is large enough for all float outputs
        float[] extremes = {Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_NORMAL,
                -Float.MAX_VALUE, -Float.MIN_VALUE, 0f, -0f,
                Float.intBitsToFloat(0x00000001), Float.intBitsToFloat(0x7f7fffff),
                Float.NaN, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY,
                1.0E7f, 0.001f, 9999999.0f};
        for (float f : extremes) {
            String s = XJBWriter.toString(f);
            assertTrue(s.length() <= NumberOutput.MAX_FLOAT_CHARS,
                () -> String.format("float %s produced '%s' (len=%d) but MAX_FLOAT_CHARS=%d",
                    f, s, s.length(), NumberOutput.MAX_FLOAT_CHARS));
        }
    }

    @Test
    public void maxDoubleCharsConstant() {
        // Verify MAX_DOUBLE_CHARS is large enough for all double outputs
        double[] extremes = {Double.MAX_VALUE, Double.MIN_VALUE, Double.MIN_NORMAL,
                -Double.MAX_VALUE, -Double.MIN_VALUE, 0d, -0d,
                Double.longBitsToDouble(1L), Double.longBitsToDouble(0x7fefffffffffffffL),
                Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY,
                1.0E7d, 0.001d, 9999999.999999998d, 4.9E-324};
        for (double d : extremes) {
            String s = XJBWriter.toString(d);
            assertTrue(s.length() <= NumberOutput.MAX_DOUBLE_CHARS,
                () -> String.format("double %s produced '%s' (len=%d) but MAX_DOUBLE_CHARS=%d",
                    d, s, s.length(), NumberOutput.MAX_DOUBLE_CHARS));
        }
    }

    // ------------------------------------------------------------------
    // Helpers
    // ------------------------------------------------------------------

    private void assertFloatOutputMatchesToString(float f) {
        String expected = XJBWriter.toString(f);
        char[] buf = new char[NumberOutput.MAX_FLOAT_CHARS];
        int pos = NumberOutput.outputFloat(f, buf, 0);
        String actual = new String(buf, 0, pos);
        assertEquals(expected, actual,
            () -> String.format("float %s (bits=0x%08X): toString='%s', outputFloat='%s'",
                f, Float.floatToRawIntBits(f), expected, actual));
    }

    private void assertDoubleOutputMatchesToString(double d) {
        String expected = XJBWriter.toString(d);
        char[] buf = new char[NumberOutput.MAX_DOUBLE_CHARS];
        int pos = NumberOutput.outputDouble(d, buf, 0);
        String actual = new String(buf, 0, pos);
        assertEquals(expected, actual,
            () -> String.format("double %s (bits=0x%016X): toString='%s', outputDouble='%s'",
                d, Double.doubleToRawLongBits(d), expected, actual));
    }

    private void assertFloatOutputWithOffset(float f, int offset) {
        String expected = XJBWriter.toString(f);
        char[] buf = new char[offset + NumberOutput.MAX_FLOAT_CHARS];
        for (int i = 0; i < offset; i++) {
            buf[i] = '$';
        }
        int pos = NumberOutput.outputFloat(f, buf, offset);
        String actual = new String(buf, offset, pos - offset);
        assertEquals(expected, actual,
            () -> String.format("float %s with offset %d: expected='%s', got='%s'", f, offset, expected, actual));
        for (int i = 0; i < offset; i++) {
            assertEquals('$', buf[i], "Offset area corrupted at position " + i);
        }
    }

    private void assertDoubleOutputWithOffset(double d, int offset) {
        String expected = XJBWriter.toString(d);
        char[] buf = new char[offset + NumberOutput.MAX_DOUBLE_CHARS];
        for (int i = 0; i < offset; i++) {
            buf[i] = '$';
        }
        int pos = NumberOutput.outputDouble(d, buf, offset);
        String actual = new String(buf, offset, pos - offset);
        assertEquals(expected, actual,
            () -> String.format("double %s with offset %d: expected='%s', got='%s'", d, offset, expected, actual));
        for (int i = 0; i < offset; i++) {
            assertEquals('$', buf[i], "Offset area corrupted at position " + i);
        }
    }
}