Skip to content
Merged
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
6 changes: 6 additions & 0 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,12 @@ Values are decoded eagerly in `_nextToken` (numbers into the narrowest of int/lo
strings into a `String`), so the accessors only return fields. `nextToken()` after end of
input or after `close()` returns null and clears the current token.

A float keeps the width it had on the wire, as in Jackson's CBOR parser: a float32 is reported
as `NumberType.FLOAT` and `NumberTypeFP.FLOAT32`, so untyped binding gives a `Float`, and its
text is the float's own (`0.1`, not `0.10000000149011612`); a float64 is `DOUBLE` and
`DOUBLE64`. Both are held in one `double`, which represents every float exactly, so the numeric
conversions are shared.

### 3.3 Property-name canonicalization

Property names go through the same `ByteQuadsCanonicalizer` that Jackson's JSON, CBOR and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ public class MessagePackParser

private enum Type
{
INT, LONG, DOUBLE, STRING, BYTES, BOOL, BIG_INT, EXT, NULL
INT, LONG, FLOAT, DOUBLE, STRING, BYTES, BOOL, BIG_INT, EXT, NULL
}
private Type type;
private int intValue;
private long longValue;
// Holds FLOAT (a float32 on the wire) as well as DOUBLE: a double represents every float
// exactly, so the numeric conversions are shared and only the reported type differs.
private double doubleValue;
private boolean booleanValue;
private byte[] bytesValue;
Expand Down Expand Up @@ -231,10 +233,10 @@ else if (streamReadContext.inObject()) {
}
break;
case FLOAT:
type = Type.DOUBLE;
type = format == MessageFormat.FLOAT32 ? Type.FLOAT : Type.DOUBLE;
doubleValue = reader.unpackDouble();
if (isObjectValueSet) {
streamReadContext.setCurrentName(String.valueOf(doubleValue));
streamReadContext.setCurrentName(floatingText());
nextToken = JsonToken.PROPERTY_NAME;
}
else {
Expand Down Expand Up @@ -339,8 +341,9 @@ public String getString()
return String.valueOf(intValue);
case LONG:
return String.valueOf(longValue);
case FLOAT:
case DOUBLE:
return String.valueOf(doubleValue);
return floatingText();
case BOOL:
return Boolean.toString(booleanValue);
case BIG_INT:
Expand Down Expand Up @@ -427,6 +430,7 @@ public byte[] getBinaryValue(Base64Variant b64variant)
return extensionTypeValue.getData();
case INT:
case LONG:
case FLOAT:
case DOUBLE:
case BOOL:
case BIG_INT:
Expand All @@ -448,6 +452,8 @@ public Number getNumberValue()
return intValue;
case LONG:
return longValue;
case FLOAT:
return (float) doubleValue;
case DOUBLE:
return doubleValue;
case BIG_INT:
Expand Down Expand Up @@ -477,6 +483,7 @@ public int getIntValue()
return _reportError("Numeric value (" + longValue + ") out of range for `int`");
}
return (int) longValue;
case FLOAT:
case DOUBLE:
if (!Double.isFinite(doubleValue)) {
return _reportError("Cannot convert non-finite double (" + doubleValue + ") to `int`");
Expand Down Expand Up @@ -514,6 +521,7 @@ public long getLongValue()
return intValue;
case LONG:
return longValue;
case FLOAT:
case DOUBLE:
if (!Double.isFinite(doubleValue)) {
return _reportError("Cannot convert non-finite double (" + doubleValue + ") to `long`");
Expand Down Expand Up @@ -552,6 +560,7 @@ public BigInteger getBigIntegerValue()
return BigInteger.valueOf(intValue);
case LONG:
return BigInteger.valueOf(longValue);
case FLOAT:
case DOUBLE:
if (!Double.isFinite(doubleValue)) {
return _reportError("Cannot convert non-finite double (" + doubleValue + ") to BigInteger");
Expand Down Expand Up @@ -583,6 +592,7 @@ public float getFloatValue()
return (float) intValue;
case LONG:
return (float) longValue;
case FLOAT:
case DOUBLE:
return (float) doubleValue;
case BIG_INT:
Expand Down Expand Up @@ -611,6 +621,7 @@ public double getDoubleValue()
return intValue;
case LONG:
return (double) longValue;
case FLOAT:
case DOUBLE:
return doubleValue;
case BIG_INT:
Expand All @@ -637,11 +648,12 @@ public BigDecimal getDecimalValue()
return BigDecimal.valueOf(intValue);
case LONG:
return BigDecimal.valueOf(longValue);
case FLOAT:
case DOUBLE:
if (!Double.isFinite(doubleValue)) {
return _reportError("Cannot convert non-finite double (" + doubleValue + ") to BigDecimal");
}
return BigDecimal.valueOf(doubleValue);
return new BigDecimal(floatingText());
Comment on lines +651 to +656

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 0fe509a: aFloat32IsReportedAsAFloat now asserts getDecimalValue() is new BigDecimal("0.1") for the float32 0.1f (and for the float64 0.1). With the old widened conversion (BigDecimal.valueOf(doubleValue)) put back temporarily, it fails with 0.10000000149011612.

case BIG_INT:
return new BigDecimal(biValue);
case NULL:
Expand Down Expand Up @@ -685,6 +697,7 @@ public Object getEmbeddedObject()
}
case INT:
case LONG:
case FLOAT:
case DOUBLE:
case BOOL:
case BIG_INT:
Expand All @@ -707,6 +720,8 @@ public NumberType getNumberType()
return NumberType.INT;
case LONG:
return NumberType.LONG;
case FLOAT:
return NumberType.FLOAT;
case DOUBLE:
return NumberType.DOUBLE;
case BIG_INT:
Expand All @@ -722,6 +737,24 @@ public NumberType getNumberType()
}
}

@Override
public NumberTypeFP getNumberTypeFP()
{
if (type == Type.FLOAT) {
return NumberTypeFP.FLOAT32;
}
if (type == Type.DOUBLE) {
return NumberTypeFP.DOUBLE64;
}
return NumberTypeFP.UNKNOWN;
}

// The shortest text of the floating-point value, as the type it had on the wire.
private String floatingText()
{
return type == Type.FLOAT ? Float.toString((float) doubleValue) : Double.toString(doubleValue);
}

@Override
protected void _closeInput() throws IOException
{
Expand Down Expand Up @@ -819,7 +852,7 @@ public void assignCurrentValue(Object v)
@Override
public boolean isNaN()
{
if (type == Type.DOUBLE) {
if (type == Type.FLOAT || type == Type.DOUBLE) {
return Double.isNaN(doubleValue) || Double.isInfinite(doubleValue);
}
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
Expand Down Expand Up @@ -176,7 +177,7 @@ else if (k.equals("array")) {
// #7
bitmap |= 1 << 8;
@SuppressWarnings("unchecked")
List<? extends Serializable> expected = Arrays.asList((double) Float.MIN_VALUE, null, "array_child_str");
List<? extends Serializable> expected = Arrays.asList(Float.MIN_VALUE, null, "array_child_str");
assertEquals(expected, v);
}
else if (k.equals("bool")) {
Expand Down Expand Up @@ -265,7 +266,7 @@ public void testParserShouldReadArray()
// #4
assertEquals(Long.MIN_VALUE, array.get(i++));
// #5
assertEquals(Float.MAX_VALUE, (Double) array.get(i++), 0.001f);
assertEquals(Float.MAX_VALUE, array.get(i++));
// #6
assertEquals(Double.MIN_VALUE, (Double) array.get(i++), 0.001f);
// #7
Expand Down Expand Up @@ -970,6 +971,79 @@ public Object deserialize(byte[] value)
}
}

// A float32 on the wire is reported as a float, as Jackson's CBOR parser does, so its type
// and its shortest text survive; a float64 stays a double.
@Test
public void aFloat32IsReportedAsAFloat() throws IOException
{
MessagePacker packer = MessagePack.newDefaultPacker(out);
packer.packArrayHeader(2).packFloat(0.1f).packDouble(0.1);
packer.close();

try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) {
assertEquals(JsonToken.START_ARRAY, p.nextToken());

assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
assertEquals(JsonParser.NumberType.FLOAT, p.getNumberType());
assertEquals(JsonParser.NumberTypeFP.FLOAT32, p.getNumberTypeFP());
assertEquals(Float.valueOf(0.1f), p.getNumberValue());
assertEquals(0.1f, p.getFloatValue());
assertEquals((double) 0.1f, p.getDoubleValue());
assertEquals("0.1", p.getString());
assertEquals(new BigDecimal("0.1"), p.getDecimalValue());

assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
assertEquals(JsonParser.NumberType.DOUBLE, p.getNumberType());
assertEquals(JsonParser.NumberTypeFP.DOUBLE64, p.getNumberTypeFP());
assertEquals(Double.valueOf(0.1), p.getNumberValue());
assertEquals("0.1", p.getString());
assertEquals(new BigDecimal("0.1"), p.getDecimalValue());
}
}

// Untyped binding keeps the width of the number on the wire.
@Test
public void anUntypedFloat32ReadsBackAsAFloat() throws IOException
{
MessagePacker packer = MessagePack.newDefaultPacker(out);
packer.packMapHeader(3)
.packString("f").packFloat(0.1f)
.packString("d").packDouble(0.1)
.packString("nan").packFloat(Float.NaN);
packer.close();

Map<String, Object> map = objectMapper.readValue(out.toByteArray(), new TypeReference<Map<String, Object>>() {});
assertEquals(Float.valueOf(0.1f), map.get("f"));
assertEquals(Double.valueOf(0.1), map.get("d"));
assertEquals(Float.valueOf(Float.NaN), map.get("nan"));
}

// A Float written into a Map comes back as the same Float through untyped binding.
@Test
public void aFloatRoundTripsThroughUntypedBinding() throws IOException
{
Map<String, Object> in = new LinkedHashMap<>();
in.put("f", 0.1f);
in.put("d", 0.1);
byte[] bytes = objectMapper.writeValueAsBytes(in);
assertEquals(in, objectMapper.readValue(bytes, new TypeReference<Map<String, Object>>() {}));
}

// A float32 key is named by the float's own text, not its widened double.
@Test
public void aFloat32KeyIsNamedByItsFloatText() throws IOException
{
MessagePacker packer = MessagePack.newDefaultPacker(out);
packer.packMapHeader(1).packFloat(0.1f).packInt(1);
packer.close();

try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) {
assertEquals(JsonToken.START_OBJECT, p.nextToken());
assertEquals(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("0.1", p.currentName());
}
}

@Test
public void parserShouldReadStrAsBin()
throws IOException
Expand Down
Loading