Skip to content
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

JsonWire: Improve handling of double special values fixes #982 #983

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 20 additions & 4 deletions src/main/java/net/openhft/chronicle/wire/JSONWire.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 10 additions & 14 deletions src/main/java/net/openhft/chronicle/wire/YamlWireOut.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -1037,31 +1037,27 @@ else if (ad7 < 1e4)
bytes.append(d);
}
} else {
bytes.append(doubleToString(d));
writeSpecialDoubleValueToBytes(bytes, d);
}
elementSeparator();

return wireOut();
}

/**
* 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
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/net/openhft/chronicle/wire/JSONNanTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* For implementation details please see:
* <ul>
* <li>{@link YamlWireOut.YamlValueOut#float64(double)}</li>
* <li>{@link YamlWireOut.YamlValueOut#float32(float)}</li>
* <li>{@link YamlWire.YamlValueOut#writeSpecialDoubleValueToBytes(Bytes, double)} </li>
* <li>{@link YamlWire.YamlValueOut#writeSpecialFloatValueToBytes(Bytes, float)} </li>
* <li>{@link JSONWire.JSONValueOut#writeSpecialDoubleValueToBytes(Bytes, double)} </li>
* <li>{@link JSONWire.JSONValueOut#writeSpecialFloatValueToBytes(Bytes, float)} </li>
* </ul>
*/
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<DoubleTestInput> 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<FloatTestInput> 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<Float> expectOutputFloatToMatchThisPredicate; // No dedicated FloatPredicate in JDK

private FloatTestInput(float inputValue,
String expectedStringRepresentation,
Predicate<Float> 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;
}
}

}