-
Notifications
You must be signed in to change notification settings - Fork 0
Json numeric type design #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7186065
2e222bd
1196242
3f62ed2
0903da3
07c0545
46da612
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String, Object>) nativeValue; | ||
|
|
||
| assertThat(map.get("smallInt")).isEqualTo(42L); | ||
| assertThat(map.get("decimal")).isInstanceOf(BigDecimal.class); | ||
| assertThat((BigDecimal) map.get("decimal")).isEqualByComparingTo(new BigDecimal("5.5")); | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
| * | ||
| * <p> | ||
| * 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; | ||
| } | ||
|
Comment on lines
53
to
66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example method can be made clearer and more consistent. The first part defines a 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()))); | ||
|
Comment on lines
+123
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| }; | ||
| } | ||
|
|
||
| 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() {} | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic in this test can be made clearer and more consistent. The first part of the test defines a
BigDecimalvariablethousand, but the second part creates aBigDecimalinline. For better readability and to more clearly demonstrate the policies, consider defining separate, descriptively named variables for both cases. This makes the intent of each part of the test more explicit.