diff --git a/src/com/amazon/ion/impl/bin/IonEncoder_1_1.java b/src/com/amazon/ion/impl/bin/IonEncoder_1_1.java index fe379df851..11218dc8cd 100644 --- a/src/com/amazon/ion/impl/bin/IonEncoder_1_1.java +++ b/src/com/amazon/ion/impl/bin/IonEncoder_1_1.java @@ -1,5 +1,6 @@ package com.amazon.ion.impl.bin; +import com.amazon.ion.Decimal; import com.amazon.ion.IonType; import com.amazon.ion.Timestamp; @@ -158,6 +159,50 @@ public static int writeFloat(WriteBuffer buffer, final double value) { } } + public static int writeDecimalValue(WriteBuffer buffer, final BigDecimal value) { + if (value == null) { + return writeNullValue(buffer, IonType.DECIMAL); + } + + int exponent = -value.scale(); + + if (BigDecimal.ZERO.compareTo(value) == 0 && !Decimal.isNegativeZero(value)) { + if (exponent == 0) { + buffer.writeByte(OpCodes.DECIMAL_ZERO_LENGTH); + return 1; + } else { + // A decimal with a coefficient of +0 is encoded using opcode 6F. + // The opcode is followed by a FlexInt representing the exponent. + buffer.writeByte(OpCodes.POSITIVE_ZERO_DECIMAL); + return 1 + buffer.writeFlexInt(exponent); + } + } + + BigInteger coefficient = value.unscaledValue(); + int numCoefficientBytes = WriteBuffer.flexIntLength(coefficient); + + int numExponentBytes = 0; + if (exponent != 0) { + numExponentBytes = WriteBuffer.fixedIntLength(exponent); + } + + int opCodeAndLengthBytes = 1; + if (numExponentBytes + numCoefficientBytes < 15) { + int opCode = OpCodes.DECIMAL_ZERO_LENGTH + numExponentBytes + numCoefficientBytes; + buffer.writeByte((byte) opCode); + } else { + // Decimal values that require more than 14 bytes can be encoded using the variable-length decimal opcode: 0xF6. + buffer.writeByte(OpCodes.VARIABLE_LENGTH_DECIMAL); + opCodeAndLengthBytes += buffer.writeFlexUInt(numExponentBytes + numCoefficientBytes); + } + buffer.writeFlexInt(coefficient); + if (exponent != 0) { + buffer.writeFixedInt(exponent); + } + + return opCodeAndLengthBytes + numCoefficientBytes + numExponentBytes; + } + /** * Writes a Timestamp to the given WriteBuffer using the Ion 1.1 encoding for Ion Timestamps. * @return the number of bytes written diff --git a/src/com/amazon/ion/impl/bin/OpCodes.java b/src/com/amazon/ion/impl/bin/OpCodes.java index 4e3f3c6468..ecfc14e8b0 100644 --- a/src/com/amazon/ion/impl/bin/OpCodes.java +++ b/src/com/amazon/ion/impl/bin/OpCodes.java @@ -18,7 +18,7 @@ private OpCodes() {} public static final byte DECIMAL_ZERO_LENGTH = 0x60; // 0x61-0x6E are additional lengths of decimals. - public static final byte NEGATIVE_ZERO_DECIMAL = 0x6F; + public static final byte POSITIVE_ZERO_DECIMAL = 0x6F; public static final byte TIMESTAMP_YEAR_PRECISION = 0x70; public static final byte TIMESTAMP_MONTH_PRECISION = 0x71; @@ -39,5 +39,6 @@ private OpCodes() {} public static final byte NULL_TYPED = (byte) 0xEB; public static final byte VARIABLE_LENGTH_INTEGER = (byte) 0xF5; + public static final byte VARIABLE_LENGTH_DECIMAL = (byte) 0xF6; public static final byte VARIABLE_LENGTH_TIMESTAMP = (byte) 0xF7; } diff --git a/test/com/amazon/ion/impl/bin/IonEncoder_1_1Test.java b/test/com/amazon/ion/impl/bin/IonEncoder_1_1Test.java index fd756db309..2f25adb22b 100644 --- a/test/com/amazon/ion/impl/bin/IonEncoder_1_1Test.java +++ b/test/com/amazon/ion/impl/bin/IonEncoder_1_1Test.java @@ -1,5 +1,6 @@ package com.amazon.ion.impl.bin; +import com.amazon.ion.Decimal; import com.amazon.ion.IonType; import com.amazon.ion.Timestamp; import org.junit.jupiter.api.Assertions; @@ -219,6 +220,61 @@ public void testWriteFloatValueForDouble(double value, String expectedBytes) { assertWritingValue(expectedBytes, value, IonEncoder_1_1::writeFloat); } + @ParameterizedTest + @CsvSource({ + " 0., 60", + " 0e1, 6F 03", + " 0e63, 6F 7F", + " 0e99, 6F 8E 01", + " 0.0, 6F FF", + " 0.00, 6F FD", + " 0.000, 6F FB", + " 0e-64, 6F 81", + " 0e-99, 6F 76 FE", + " -0., 61 01", + " -0e1, 62 01 01", + " -0e3, 62 01 03", + " -0e127, 62 01 7F", + " -0e199, 63 01 C7 00", + " -0e-1, 62 01 FF", + " -0e-2, 62 01 FE", + " -0e-3, 62 01 FD", + " -0e-127, 62 01 81", + " -0e-199, 63 01 39 FF", + " 0.01, 62 03 FE", + " 0.1, 62 03 FF", + " 1, 61 03", + " 1e1, 62 03 01", + " 1e2, 62 03 02", + " 1e127, 62 03 7F", + " 1e128, 63 03 80 00", + " 1e65536, 64 03 00 00 01", + " 2, 61 05", + " 7, 61 0F", + " 14, 61 1D", + " 1.0, 62 15 FF", + " 1.00, 63 92 01 FE", + " 1.27, 63 FE 01 FE", + " 3.142, 63 1A 31 FD", + " 3.14159, 64 7C 59 26 FB", + " 3.141593, 65 98 FD FE 02 FA", + " 3.141592653, 66 B0 C9 1C 68 17 F7", + " 3.14159265359, 67 E0 93 7D 56 49 12 F5", + " 3.1415926535897932, 69 80 4C 43 76 65 9E 9C 6F F0", + " 3.1415926535897932384626434, 6E 00 50 E0 DC F7 CC D6 08 48 99 92 3F 03 E7", + "3.141592653589793238462643383, F6 1F 00 E0 2D 8F A4 21 D0 E7 46 C0 87 AA 89 02 E5", + }) + public void testWriteDecimalValue(@ConvertWith(StringToDecimal.class) Decimal value, String expectedBytes) { + assertWritingValue(expectedBytes, value, IonEncoder_1_1::writeDecimalValue); + } + + @Test + public void testWriteDecimalValueForNull() { + int numBytes = IonEncoder_1_1.writeDecimalValue(buf, null); + Assertions.assertEquals("EB 03", byteArrayToHex(bytes())); + Assertions.assertEquals(2, numBytes); + } + // Because timestamp subfields are smeared across bytes, it's easier to reason about them in 1s and 0s // instead of hex digits @ParameterizedTest @@ -457,4 +513,19 @@ protected Timestamp convert(String source) throws ArgumentConversionException { return Timestamp.valueOf(source); } } + + /** + * Converts a String to a Decimal for a @Parameterized test + */ + static class StringToDecimal extends TypedArgumentConverter { + protected StringToDecimal() { + super(String.class, Decimal.class); + } + + @Override + protected Decimal convert(String source) throws ArgumentConversionException { + if (source == null) return null; + return Decimal.valueOf(source); + } + } }