diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 7975f14..c716be4 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -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 diff --git a/src/main/java/org/komamitsu/jackson/dataformat/msgpack/MessagePackParser.java b/src/main/java/org/komamitsu/jackson/dataformat/msgpack/MessagePackParser.java index 02bce52..060e2c9 100644 --- a/src/main/java/org/komamitsu/jackson/dataformat/msgpack/MessagePackParser.java +++ b/src/main/java/org/komamitsu/jackson/dataformat/msgpack/MessagePackParser.java @@ -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; @@ -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 { @@ -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: @@ -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: @@ -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: @@ -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`"); @@ -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`"); @@ -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"); @@ -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: @@ -611,6 +621,7 @@ public double getDoubleValue() return intValue; case LONG: return (double) longValue; + case FLOAT: case DOUBLE: return doubleValue; case BIG_INT: @@ -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()); case BIG_INT: return new BigDecimal(biValue); case NULL: @@ -685,6 +697,7 @@ public Object getEmbeddedObject() } case INT: case LONG: + case FLOAT: case DOUBLE: case BOOL: case BIG_INT: @@ -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: @@ -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 { @@ -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; diff --git a/src/test/java/org/komamitsu/jackson/dataformat/msgpack/MessagePackParserTest.java b/src/test/java/org/komamitsu/jackson/dataformat/msgpack/MessagePackParserTest.java index 808683b..f1de1b5 100644 --- a/src/test/java/org/komamitsu/jackson/dataformat/msgpack/MessagePackParserTest.java +++ b/src/test/java/org/komamitsu/jackson/dataformat/msgpack/MessagePackParserTest.java @@ -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; @@ -176,7 +177,7 @@ else if (k.equals("array")) { // #7 bitmap |= 1 << 8; @SuppressWarnings("unchecked") - List expected = Arrays.asList((double) Float.MIN_VALUE, null, "array_child_str"); + List expected = Arrays.asList(Float.MIN_VALUE, null, "array_child_str"); assertEquals(expected, v); } else if (k.equals("bool")) { @@ -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 @@ -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 map = objectMapper.readValue(out.toByteArray(), new TypeReference>() {}); + 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 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>() {})); + } + + // 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