diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96e5cf7..af21fa8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: for k in totals: totals[k]+=int(r.get(k,'0')) except Exception: pass - exp_tests=511 + exp_tests=519 exp_skipped=0 if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped: print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}") diff --git a/README.md b/README.md index 0e9328b..93499ac 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,15 @@ java -cp ./json-java21/target/java.util.json-*.jar:./json-java21/target/test-cla *Replace `*` with the actual version number from the JAR filename.* +## Numeric handling (JsonNumber and native Java numbers) + +Prior versions of this backport included convenience entry points for building JSON numbers from `BigDecimal` / `BigInteger`. Upstream has moved away from that shape: `JsonNumber` preserves the JSON number text and you convert explicitly to native Java numeric types depending on your policy (lossless via `toString()` + `BigDecimal`, or range/precision-limited via `toLong()` / `toDouble()`). + +For runnable examples (including counter-examples where conversions throw or lose precision, plus a pattern-matching “map to native types” helper), see: + +- `json-java21/src/test/java/jdk/sandbox/java/util/json/examples/DesignChoicesExamples.java` +- `json-java21/src/test/java/jdk/sandbox/java/util/json/DesignChoicesNumberExamplesTest.java` + ## API Overview The API provides immutable JSON value types: diff --git a/json-java21/src/test/java/jdk/sandbox/java/util/json/DesignChoicesNumberExamplesTest.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/DesignChoicesNumberExamplesTest.java new file mode 100644 index 0000000..bed71e8 --- /dev/null +++ b/json-java21/src/test/java/jdk/sandbox/java/util/json/DesignChoicesNumberExamplesTest.java @@ -0,0 +1,121 @@ +package jdk.sandbox.java.util.json; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Map; +import java.util.logging.Logger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DesignChoicesNumberExamplesTest { + private static final Logger LOGGER = Logger.getLogger(DesignChoicesNumberExamplesTest.class.getName()); + + @Test + void parseHighPrecisionNumberIsLosslessViaToString() { + LOGGER.info("Executing parseHighPrecisionNumberIsLosslessViaToString"); + + var text = "3.141592653589793238462643383279"; + var n = (JsonNumber) Json.parse(text); + + assertThat(n.toString()).isEqualTo(text); + assertThat(new BigDecimal(n.toString())).isEqualByComparingTo(new BigDecimal(text)); + } + + @Test + void convertingToDoubleIsPotentiallyLossy() { + LOGGER.info("Executing convertingToDoubleIsPotentiallyLossy"); + + var text = "3.141592653589793238462643383279"; + var n = (JsonNumber) Json.parse(text); + + var lossless = new BigDecimal(n.toString()); + var lossy = new BigDecimal(Double.toString(n.toDouble())); + + assertThat(lossy).isNotEqualByComparingTo(lossless); + } + + @Test + void convertingNonIntegralNumberToLongThrows() { + LOGGER.info("Executing convertingNonIntegralNumberToLongThrows"); + + var n = (JsonNumber) Json.parse("5.5"); + assertThatThrownBy(n::toLong) + .isInstanceOf(JsonAssertionException.class); + } + + @Test + void convertingOutOfRangeNumberToDoubleThrows() { + LOGGER.info("Executing convertingOutOfRangeNumberToDoubleThrows"); + + var n = (JsonNumber) Json.parse("1e309"); + assertThatThrownBy(n::toDouble) + .isInstanceOf(JsonAssertionException.class); + } + + @Test + void parseExponentFormToBigIntegerExactWorks() { + LOGGER.info("Executing parseExponentFormToBigIntegerExactWorks"); + + var n = (JsonNumber) Json.parse("1.23e2"); + BigInteger bi = new BigDecimal(n.toString()).toBigIntegerExact(); + + assertThat(bi).isEqualTo(BigInteger.valueOf(123)); + } + + @Test + void bigDecimalToJsonNumberRequiresChoosingATextPolicy() { + LOGGER.info("Executing bigDecimalToJsonNumberRequiresChoosingATextPolicy"); + + // Using toPlainString() for a plain number representation + var bdPlain = new BigDecimal("1000"); + + var plain = JsonNumber.of(bdPlain.toPlainString()); + assertThat(plain.toString()).isEqualTo("1000"); + + // Using toString(), which may produce scientific notation + var bdScientific = new BigDecimal("1E+3"); + var scientific = JsonNumber.of(bdScientific.toString()); + assertThat(scientific.toString()).isEqualTo("1E+3"); + } + + @Test + void lexicalPreservationDiffersFromNumericNormalization() { + LOGGER.info("Executing lexicalPreservationDiffersFromNumericNormalization"); + + var a = (JsonNumber) Json.parse("1e2"); + var b = (JsonNumber) Json.parse("100"); + + // lexical forms differ + assertThat(a.toString()).isEqualTo("1e2"); + assertThat(b.toString()).isEqualTo("100"); + + // but numeric values compare equal when canonicalized explicitly + assertThat(new BigDecimal(a.toString())).isEqualByComparingTo(new BigDecimal(b.toString())); + } + + @Test + void mappingToNativeTypesUsesPatternMatchingAndExplicitNumberPolicy() { + LOGGER.info("Executing mappingToNativeTypesUsesPatternMatchingAndExplicitNumberPolicy"); + + JsonValue json = Json.parse(""" + { + "smallInt": 42, + "decimal": 5.5 + } + """); + + Object nativeValue = jdk.sandbox.java.util.json.examples.DesignChoicesExamples.toNative(json); + assertThat(nativeValue).isInstanceOf(Map.class); + + @SuppressWarnings("unchecked") + var map = (Map) nativeValue; + + assertThat(map.get("smallInt")).isEqualTo(42L); + assertThat(map.get("decimal")).isInstanceOf(BigDecimal.class); + assertThat((BigDecimal) map.get("decimal")).isEqualByComparingTo(new BigDecimal("5.5")); + } +} + diff --git a/json-java21/src/test/java/jdk/sandbox/java/util/json/examples/DesignChoicesExamples.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/examples/DesignChoicesExamples.java new file mode 100644 index 0000000..436df70 --- /dev/null +++ b/json-java21/src/test/java/jdk/sandbox/java/util/json/examples/DesignChoicesExamples.java @@ -0,0 +1,153 @@ +package jdk.sandbox.java.util.json.examples; + +import jdk.sandbox.java.util.json.Json; +import jdk.sandbox.java.util.json.JsonAssertionException; +import jdk.sandbox.java.util.json.JsonArray; +import jdk.sandbox.java.util.json.JsonBoolean; +import jdk.sandbox.java.util.json.JsonNumber; +import jdk.sandbox.java.util.json.JsonNull; +import jdk.sandbox.java.util.json.JsonObject; +import jdk.sandbox.java.util.json.JsonString; +import jdk.sandbox.java.util.json.JsonValue; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Standalone examples demonstrating numeric design choices. + * + *

+ * See the repository README for context and pointers. + */ +public final class DesignChoicesExamples { + + public static void main(String[] args) { + System.out.println("=== Numeric design choices examples ===\n"); + + parseToBigDecimalLossless(); + parseToBigIntegerExact(); + bigDecimalToJsonNumberChooseTextPolicy(); + lexicalPreservationNotNormalization(); + counterExamples(); + mappingToNativeTypesWithPatternMatching(); + + System.out.println("\n=== All examples completed successfully! ==="); + } + + public static BigDecimal parseToBigDecimalLossless() { + var n = (JsonNumber) Json.parse("3.141592653589793238462643383279"); + var bd = new BigDecimal(n.toString()); + System.out.println("lossless BigDecimal: " + bd.toPlainString()); + return bd; + } + + public static BigInteger parseToBigIntegerExact() { + var n = (JsonNumber) Json.parse("1.23e2"); + var bi = new BigDecimal(n.toString()).toBigIntegerExact(); + System.out.println("exact BigInteger: " + bi); + return bi; + } + + public static JsonNumber bigDecimalToJsonNumberChooseTextPolicy() { + // Example with toPlainString() to avoid scientific notation. + var bdPlain = new BigDecimal("1000"); + + var plain = JsonNumber.of(bdPlain.toPlainString()); + System.out.println("BigDecimal.toPlainString() -> JsonNumber: " + plain); + + // Example with toString(), which may use scientific notation. + var bdScientific = new BigDecimal("1E+3"); + var scientific = JsonNumber.of(bdScientific.toString()); + System.out.println("BigDecimal.toString() -> JsonNumber: " + scientific); + + return plain; + } + + public static boolean lexicalPreservationNotNormalization() { + var a = (JsonNumber) Json.parse("1e2"); + var b = (JsonNumber) Json.parse("100"); + + System.out.println("a.toString(): " + a); + System.out.println("b.toString(): " + b); + System.out.println("same numeric value? " + new BigDecimal(a.toString()).compareTo(new BigDecimal(b.toString()))); + + return a.toString().equals(b.toString()); + } + + public static void counterExamples() { + System.out.println("Counter-examples"); + System.out.println("----------------"); + + // 1) Converting a non-integral JSON number to long throws. + try { + var nonIntegral = (JsonNumber) Json.parse("5.5"); + System.out.println("nonIntegral.toString(): " + nonIntegral); + System.out.println("nonIntegral.toLong(): " + nonIntegral.toLong()); + } catch (JsonAssertionException e) { + System.out.println("Expected toLong() failure: " + e.getMessage()); + } + + // 2) Converting an out-of-range JSON number to double throws. + try { + var tooBig = (JsonNumber) Json.parse("1e309"); + System.out.println("tooBig.toString(): " + tooBig); + System.out.println("tooBig.toDouble(): " + tooBig.toDouble()); + } catch (JsonAssertionException e) { + System.out.println("Expected toDouble() failure: " + e.getMessage()); + } + + // 3) Converting to double can be lossy even when it does not throw. + var highPrecision = (JsonNumber) Json.parse("3.141592653589793238462643383279"); + var lossless = new BigDecimal(highPrecision.toString()); + var lossy = new BigDecimal(Double.toString(highPrecision.toDouble())); + System.out.println("lossless (BigDecimal): " + lossless.toPlainString()); + System.out.println("lossy (double->BD): " + lossy.toPlainString()); + System.out.println("lossless equals lossy? " + (lossless.compareTo(lossy) == 0)); + System.out.println(); + } + + public static Object toNative(JsonValue v) { + return switch (v) { + case JsonNull ignored -> null; + case JsonBoolean b -> b.bool(); + case JsonString s -> s.string(); + case JsonNumber n -> { + try { + yield n.toLong(); + } catch (JsonAssertionException ignored) { + yield new BigDecimal(n.toString()); + } + } + case JsonArray a -> a.elements().stream().map(DesignChoicesExamples::toNative).toList(); + case JsonObject o -> o.members().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> toNative(e.getValue()))); + }; + } + + public static Object mappingToNativeTypesWithPatternMatching() { + System.out.println("Mapping to native types (pattern matching)"); + System.out.println("------------------------------------------"); + + JsonValue json = Json.parse(""" + { + "smallInt": 42, + "decimal": 5.5, + "huge": 3.141592653589793238462643383279, + "flag": true, + "name": "Ada", + "items": [1, 2, 3] + } + """); + + Object nativeValue = toNative(json); + System.out.println("native class: " + nativeValue.getClass().getName()); + System.out.println("native value: " + nativeValue); + System.out.println(); + return nativeValue; + } + + private DesignChoicesExamples() {} +} +