diff --git a/README.adoc b/README.adoc
index e0808b391..7c968d589 100644
--- a/README.adoc
+++ b/README.adoc
@@ -743,6 +743,26 @@ class Data implements Marshallable {
}
----
+== Wire type implementation details
+
+=== JSONWire
+
+==== Handling of special floating point values such as Double.NaN
+
+JSONWire will serialize and deserialize special floating point values such as `Double.NaN` to and from valid JSON string literals in accordance
+with the following table:
+
+[cols="2*", options="header"]
+|=================================================
+| Java primitive value | String representation
+| Double.NaN | "NaN"
+| Double.POSITIVE_INFINITY | "Infinity"
+| Double.NEGATIVE_INFINITY | "-Infinity"
+| Float.NaN | "NaN"
+| Float.POSITIVE_INFINITY | "Infinity"
+| Float.NEGATIVE_INFINITY | "-Infinity"
+|=================================================
+
== LongConverter
Provides an abstraction for converting between long values and their string representations,
diff --git a/src/main/java/net/openhft/chronicle/wire/JSONWire.java b/src/main/java/net/openhft/chronicle/wire/JSONWire.java
index 795a5abef..0ccbc9107 100644
--- a/src/main/java/net/openhft/chronicle/wire/JSONWire.java
+++ b/src/main/java/net/openhft/chronicle/wire/JSONWire.java
@@ -955,14 +955,30 @@ protected void fieldValueSeperator() {
public void writeComment(@NotNull CharSequence s) {
}
+ /**
+ * Write a special double value (e.g. NaN) as a string to the given bytes.
+ *
+ * @param bytes The bytes to append the stringified double value to
+ * @param value The double value to convert to a string
+ */
@Override
- protected String doubleToString(double d) {
- return Double.isNaN(d) ? "null" : super.doubleToString(d);
+ protected void writeSpecialDoubleValueToBytes(Bytes> bytes, double value) {
+ bytes.append('"');
+ bytes.append(Double.toString(value));
+ bytes.append('"');
}
+ /**
+ * Write a special double value (e.g. NaN) as a string to the given bytes.
+ *
+ * @param bytes The bytes to append the stringified double value to
+ * @param value The double value to convert to a string
+ */
@Override
- protected String floatToString(float f) {
- return Float.isNaN(f) ? "null" : super.floatToString(f);
+ protected void writeSpecialFloatValueToBytes(Bytes> bytes, float value) {
+ bytes.append('"');
+ bytes.append(Float.toString(value));
+ bytes.append('"');
}
@NotNull
diff --git a/src/main/java/net/openhft/chronicle/wire/YamlWireOut.java b/src/main/java/net/openhft/chronicle/wire/YamlWireOut.java
index ea251ea2a..2e42132c2 100644
--- a/src/main/java/net/openhft/chronicle/wire/YamlWireOut.java
+++ b/src/main/java/net/openhft/chronicle/wire/YamlWireOut.java
@@ -997,7 +997,7 @@ public T float32(float f) {
if (af == 0 || (af >= 1e-3 && af < 1e6))
bytes.append(f);
else
- bytes.append(floatToString(f));
+ writeSpecialFloatValueToBytes(bytes, f);
elementSeparator();
return wireOut();
@@ -1037,7 +1037,7 @@ else if (ad7 < 1e4)
bytes.append(d);
}
} else {
- bytes.append(doubleToString(d));
+ writeSpecialDoubleValueToBytes(bytes, d);
}
elementSeparator();
@@ -1045,23 +1045,19 @@ else if (ad7 < 1e4)
}
/**
- * Converts the provided double value to its corresponding String representation.
- *
- * @param d The double value to convert.
- * @return The String representation of the provided double value.
+ * Writes a special double value, e.g. NaN, to bytes in the context of Yaml Wire. For now this
+ * remains as an unquoted string representation.
*/
- protected String doubleToString(double d) {
- return Double.toString(d);
+ protected void writeSpecialDoubleValueToBytes(Bytes> bytes, double value) {
+ bytes.append(Double.toString(value));
}
/**
- * Converts the provided float value to its corresponding String representation.
- *
- * @param f The float value to convert.
- * @return The String representation of the provided float value.
+ * Writes a special double value, e.g. NaN, to bytes in the context of Yaml Wire. For now this
+ * remains as an unquoted string representation.
*/
- protected String floatToString(float f) {
- return Float.toString(f);
+ protected void writeSpecialFloatValueToBytes(Bytes> bytes, float value) {
+ bytes.append(Float.toString(value));
}
@NotNull
diff --git a/src/test/java/net/openhft/chronicle/wire/JSONNanTest.java b/src/test/java/net/openhft/chronicle/wire/JSONNanTest.java
index 4266f4224..af2dc94ff 100644
--- a/src/test/java/net/openhft/chronicle/wire/JSONNanTest.java
+++ b/src/test/java/net/openhft/chronicle/wire/JSONNanTest.java
@@ -42,7 +42,7 @@ public void writeNaNs() {
wire.write().marshallable(value);
// Assert that the wire content represents the NaN as null
- Assert.assertEquals("\"\":{\"value\":0.0,\"value1\":null,\"value2\":0,\"field\":\"text\"}", wire.toString());
+ Assert.assertEquals("\"\":{\"value\":0.0,\"value1\":\"NaN\",\"value2\":0,\"field\":\"text\"}", wire.toString());
} finally {
// Release the byte buffer resources
b.releaseLast();
diff --git a/src/test/java/net/openhft/chronicle/wire/JsonWireDoubleAndFloatSpecialValuesAcceptanceTests.java b/src/test/java/net/openhft/chronicle/wire/JsonWireDoubleAndFloatSpecialValuesAcceptanceTests.java
new file mode 100644
index 000000000..a93ea0a52
--- /dev/null
+++ b/src/test/java/net/openhft/chronicle/wire/JsonWireDoubleAndFloatSpecialValuesAcceptanceTests.java
@@ -0,0 +1,195 @@
+package net.openhft.chronicle.wire;
+
+import net.openhft.chronicle.bytes.Bytes;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.function.DoublePredicate;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Acceptance tests for JSON Wire Double and Float special value handling, e.g. NaN, Infinity.
+ *
+ * For implementation details please see:
+ *
+ * - {@link YamlWireOut.YamlValueOut#float64(double)}
+ * - {@link YamlWireOut.YamlValueOut#float32(float)}
+ * - {@link YamlWire.YamlValueOut#writeSpecialDoubleValueToBytes(Bytes, double)}
+ * - {@link YamlWire.YamlValueOut#writeSpecialFloatValueToBytes(Bytes, float)}
+ * - {@link JSONWire.JSONValueOut#writeSpecialDoubleValueToBytes(Bytes, double)}
+ * - {@link JSONWire.JSONValueOut#writeSpecialFloatValueToBytes(Bytes, float)}
+ *
+ */
+public class JsonWireDoubleAndFloatSpecialValuesAcceptanceTests {
+
+ @ParameterizedTest
+ @MethodSource("doubleTestInputs")
+ void serialiseDoubleSpecialValues(DoubleTestInput doubleTestInput) {
+ assertEquals(
+ String.format("\"%s\"", doubleTestInput.expectedStringRepresentation),
+ toJson(doubleTestInput.inputValue),
+ "Expected correct representation for special value and for it to be quoted as a string literal"
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("floatTestInputs")
+ void serialiseFloatSpecialValues(FloatTestInput floatTestInput) {
+ assertEquals(
+ String.format("\"%s\"", floatTestInput.expectedStringRepresentation),
+ toJson(floatTestInput.inputValue),
+ "Expected correct representation for special value and for it to be quoted as a string literal"
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("doubleTestInputs")
+ void doubleRoundTrip(DoubleTestInput doubleTestInput) {
+ // Serialise an object to JSON and ensure its represented correctly
+ JSONWire inputWire = new JSONWire();
+ inputWire.getValueOut().object(new DoubleDto(doubleTestInput.inputValue));
+ String text = JSONWire.asText(inputWire);
+ assertEquals(
+ String.format("{\"value\":\"%s\"}", doubleTestInput.expectedStringRepresentation),
+ text,
+ "Expected JSON representation where special values are quoted string literals"
+ );
+
+ // Deserialize back to an object, ensure that the special value is retained
+ JSONWire outputWire = JSONWire.from(text);
+ DoubleDto object = outputWire.getValueIn().object(DoubleDto.class);
+ Assertions.assertNotNull(object);
+ Assertions.assertTrue(doubleTestInput.expectOutputDoubleToMatchThisPredicate.test(object.value));
+ }
+
+ @ParameterizedTest
+ @MethodSource("floatTestInputs")
+ void floatRoundTrip(FloatTestInput floatTestInput) {
+ // Serialise an object to JSON and ensure its represented correctly
+ JSONWire inputWire = new JSONWire();
+ inputWire.getValueOut().object(new FloatDto(floatTestInput.inputValue));
+ String text = JSONWire.asText(inputWire);
+ assertEquals(
+ String.format("{\"value\":\"%s\"}", floatTestInput.expectedStringRepresentation),
+ text,
+ "Expected JSON representation where special values are quoted string literals"
+ );
+
+ // Deserialize back to an object, ensure that the special value is retained
+ JSONWire outputWire = JSONWire.from(text);
+ FloatDto object = outputWire.getValueIn().object(FloatDto.class);
+ Assertions.assertNotNull(object);
+ Assertions.assertTrue(floatTestInput.expectOutputFloatToMatchThisPredicate.test(object.value));
+ }
+
+ private static Stream doubleTestInputs() {
+ return Stream.of(
+ new DoubleTestInput(Double.NaN, "NaN", Double::isNaN),
+ new DoubleTestInput(Double.NEGATIVE_INFINITY, "-Infinity", Double::isInfinite),
+ new DoubleTestInput(Double.POSITIVE_INFINITY, "Infinity", Double::isInfinite)
+ );
+ }
+
+ private static class DoubleTestInput {
+ private final double inputValue;
+ private final String expectedStringRepresentation;
+
+ private final DoublePredicate expectOutputDoubleToMatchThisPredicate;
+
+ private DoubleTestInput(double inputValue,
+ String expectedStringRepresentation,
+ DoublePredicate expectOutputDoubleToMatchThisPredicate) {
+ this.inputValue = inputValue;
+ this.expectedStringRepresentation = expectedStringRepresentation;
+ this.expectOutputDoubleToMatchThisPredicate = expectOutputDoubleToMatchThisPredicate;
+ }
+
+ @Override
+ public String toString() {
+ return "DoubleTestInput{" +
+ "value=" + inputValue +
+ ", expectedRepresentation='" + expectedStringRepresentation + '\'' +
+ '}';
+ }
+
+ }
+
+ private static Stream floatTestInputs() {
+ return Stream.of(
+ new FloatTestInput(Float.NaN, "NaN", Double::isNaN),
+ new FloatTestInput(Float.NEGATIVE_INFINITY, "-Infinity", Double::isInfinite),
+ new FloatTestInput(Float.POSITIVE_INFINITY, "Infinity", Double::isInfinite)
+ );
+ }
+
+ private static class FloatTestInput {
+
+ private final float inputValue;
+ private final String expectedStringRepresentation;
+ private final Predicate expectOutputFloatToMatchThisPredicate; // No dedicated FloatPredicate in JDK
+
+ private FloatTestInput(float inputValue,
+ String expectedStringRepresentation,
+ Predicate expectOutputFloatToMatchThisPredicate) {
+ this.inputValue = inputValue;
+ this.expectedStringRepresentation = expectedStringRepresentation;
+ this.expectOutputFloatToMatchThisPredicate = expectOutputFloatToMatchThisPredicate;
+ }
+
+ @Override
+ public String toString() {
+ return "FloatTestInput{" +
+ "value=" + inputValue +
+ ", expectedRepresentation='" + expectedStringRepresentation + '\'' +
+ '}';
+ }
+
+ }
+
+ /**
+ * Convert the double value to its JSON string representation.
+ */
+ public String toJson(double value) {
+ JSONWire jsonWire = new JSONWire();
+ jsonWire.getValueOut().object(value);
+ return JSONWire.asText(jsonWire);
+ }
+
+ /**
+ * Convert the float value to its JSON string representation.
+ */
+ public String toJson(float value) {
+ JSONWire jsonWire = new JSONWire();
+ jsonWire.getValueOut().object(value);
+ return JSONWire.asText(jsonWire);
+ }
+
+ /**
+ * Simple DTO for testing double serialise/deserialize to/from JSON.
+ */
+ private static class DoubleDto extends SelfDescribingMarshallable {
+
+ private final double value;
+
+ private DoubleDto(double value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Simple DTO for testing float serialise/deserialize to/from JSON.
+ */
+ private static class FloatDto extends SelfDescribingMarshallable {
+
+ private final float value;
+
+ private FloatDto(float value) {
+ this.value = value;
+ }
+ }
+
+}