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 cca35421f7..0eba255930 100644 --- a/src/com/amazon/ion/impl/bin/IonEncoder_1_1.java +++ b/src/com/amazon/ion/impl/bin/IonEncoder_1_1.java @@ -1,13 +1,12 @@ package com.amazon.ion.impl.bin; -import com.amazon.ion.Decimal; import com.amazon.ion.IonType; import com.amazon.ion.Timestamp; -import com.amazon.ion.impl.bin.utf8.Utf8StringEncoder; import java.math.BigDecimal; import java.math.BigInteger; +import static com.amazon.ion.impl.bin.Ion_1_1_Constants.*; import static java.lang.Double.doubleToRawLongBits; import static java.lang.Float.floatToIntBits; @@ -158,4 +157,215 @@ public static int writeFloat(WriteBuffer buffer, final double value) { return 9; } } + + public static int writeTimestampValue(WriteBuffer buffer, Timestamp value) { + if (value == null) { + return writeNullValue(buffer, IonType.TIMESTAMP); + } + // Timestamps may be encoded using the short form if they meet the following conditions: + // -- The year is between 1970 and 2097. + if (value.getYear() < 1970 || value.getYear() > 2097) { + return writeLongFormTimestampValue(buffer, value); + } + + // If the precision is year, month, or day, we can skip the remaining checks. + if (!value.getPrecision().includes(Timestamp.Precision.MINUTE)) { + return writeShortFormTimestampValue(buffer, value); + } + + // -- The fractional seconds are a common precision. + int secondsScale = value.getDecimalSecond().scale(); + if (secondsScale != 0 && secondsScale != 3 && secondsScale != 6 && secondsScale != 9) { + return writeLongFormTimestampValue(buffer, value); + } + // -- The local offset is either UTC, unknown, or falls between -14:00 to +14:00 and is divisible by 15 minutes. + Integer offset = value.getLocalOffset(); + if (offset != null && (offset < -14 * 60 || offset > 14 * 60 || offset % 15 != 0)) { + return writeLongFormTimestampValue(buffer, value); + } + return writeShortFormTimestampValue(buffer, value); + } + + /** + * Writes a short-form timestamp. + * Value cannot be null. + * If calling from outside this class, use writeTimestampValue instead. + */ + private static int writeShortFormTimestampValue(WriteBuffer buffer, Timestamp value) { + long bits = (value.getYear() - 1970L); + if (value.getPrecision() == Timestamp.Precision.YEAR) { + buffer.writeByte(OpCodes.TIMESTAMP_YEAR_PRECISION); + buffer.writeFixedUInt(bits, 1); + return 2; + } + + bits |= ((long) value.getMonth()) << S_TIMESTAMP_MONTH_BIT_OFFSET; + if (value.getPrecision() == Timestamp.Precision.MONTH) { + buffer.writeByte(OpCodes.TIMESTAMP_MONTH_PRECISION); + buffer.writeFixedUInt(bits, 2); + return 3; + } + + bits |= ((long) value.getDay()) << S_TIMESTAMP_DAY_BIT_OFFSET; + if (value.getPrecision() == Timestamp.Precision.DAY) { + buffer.writeByte(OpCodes.TIMESTAMP_DAY_PRECISION); + buffer.writeFixedUInt(bits, 2); + return 3; + } + + bits |= ((long) value.getHour()) << S_TIMESTAMP_HOUR_BIT_OFFSET; + bits |= ((long) value.getMinute()) << S_TIMESTAMP_MINUTE_BIT_OFFSET; + if (value.getLocalOffset() == null || value.getLocalOffset() == 0) { + if (value.getLocalOffset() != null) { + bits |= S_U_TIMESTAMP_UTC_FLAG; + } + + if (value.getPrecision() == Timestamp.Precision.MINUTE) { + buffer.writeByte(OpCodes.TIMESTAMP_MINUTE_PRECISION); + buffer.writeFixedUInt(bits, 4); + return 5; + } + + bits |= ((long) value.getSecond()) << S_U_TIMESTAMP_SECOND_BIT_OFFSET; + + int secondsScale = value.getDecimalSecond().scale(); + if (secondsScale != 0) { + long fractionalSeconds = value.getDecimalSecond().remainder(BigDecimal.ONE).movePointRight(secondsScale).longValue(); + bits |= fractionalSeconds << S_U_TIMESTAMP_FRACTION_BIT_OFFSET; + } + switch (secondsScale) { + case 0: + buffer.writeByte(OpCodes.TIMESTAMP_SECOND_PRECISION); + buffer.writeFixedUInt(bits, 5); + return 6; + case 3: + buffer.writeByte(OpCodes.TIMESTAMP_MILLIS_PRECISION); + buffer.writeFixedUInt(bits, 6); + return 7; + case 6: + buffer.writeByte(OpCodes.TIMESTAMP_MICROS_PRECISION); + buffer.writeFixedUInt(bits, 7); + return 8; + case 9: + buffer.writeByte(OpCodes.TIMESTAMP_NANOS_PRECISION); + buffer.writeFixedUInt(bits, 8); + return 9; + default: + throw new IllegalStateException("This is unreachable!"); + } + } else { + long localOffset = value.getLocalOffset().longValue() / 15; + bits |= (localOffset & LEAST_SIGNIFICANT_7_BITS) << S_O_TIMESTAMP_OFFSET_BIT_OFFSET; + + if (value.getPrecision() == Timestamp.Precision.MINUTE) { + buffer.writeByte(OpCodes.TIMESTAMP_MINUTE_PRECISION_WITH_OFFSET); + buffer.writeFixedUInt(bits, 5); + return 6; + } + + bits |= ((long) value.getSecond()) << S_O_TIMESTAMP_SECOND_BIT_OFFSET; + + // The fractional seconds bits will be put into a separate long because we need nine bytes total + // if there are nanoseconds (which is too much for one long) and the boundary between the seconds + // and fractional seconds subfields conveniently aligns with a byte boundary. + long fractionBits = 0; + int secondsScale = value.getDecimalSecond().scale(); + if (secondsScale != 0) { + fractionBits = value.getDecimalSecond().remainder(BigDecimal.ONE).movePointRight(secondsScale).longValue(); + } + switch (secondsScale) { + case 0: + buffer.writeByte(OpCodes.TIMESTAMP_SECOND_PRECISION_WITH_OFFSET); + buffer.writeFixedUInt(bits, 5); + return 6; + case 3: + buffer.writeByte(OpCodes.TIMESTAMP_MILLIS_PRECISION_WITH_OFFSET); + buffer.writeFixedUInt(bits, 5); + buffer.writeFixedUInt(fractionBits, 2); + return 8; + case 6: + buffer.writeByte(OpCodes.TIMESTAMP_MICROS_PRECISION_WITH_OFFSET); + buffer.writeFixedUInt(bits, 5); + buffer.writeFixedUInt(fractionBits, 3); + return 9; + case 9: + buffer.writeByte(OpCodes.TIMESTAMP_NANOS_PRECISION_WITH_OFFSET); + buffer.writeFixedUInt(bits, 5); + buffer.writeFixedUInt(fractionBits, 4); + return 10; + default: + throw new IllegalStateException("This is unreachable!"); + } + } + } + + /** + * Writes a long-form timestamp. + * Value may not be null. + * Only visible for testing. If calling from outside this class, use writeTimestampValue instead. + */ + static int writeLongFormTimestampValue(WriteBuffer buffer, Timestamp value) { + buffer.writeByte(OpCodes.VARIABLE_LENGTH_TIMESTAMP); + + long bits = value.getYear(); + if (value.getPrecision() == Timestamp.Precision.YEAR) { + buffer.writeFlexUInt(2); + buffer.writeFixedUInt(bits, 2); + return 4; // OpCode + FlexUInt + 2 bytes data + } + + bits |= ((long) value.getMonth()) << L_TIMESTAMP_MONTH_BIT_OFFSET; + if (value.getPrecision() == Timestamp.Precision.MONTH) { + buffer.writeFlexUInt(3); + buffer.writeFixedUInt(bits, 3); + return 5; // OpCode + FlexUInt + 3 bytes data + } + + bits |= ((long) value.getDay()) << L_TIMESTAMP_DAY_BIT_OFFSET; + if (value.getPrecision() == Timestamp.Precision.DAY) { + buffer.writeFlexUInt(3); + buffer.writeFixedUInt(bits, 3); + return 5; // OpCode + FlexUInt + 3 bytes data + } + + bits |= ((long) value.getHour()) << L_TIMESTAMP_HOUR_BIT_OFFSET; + bits |= ((long) value.getMinute()) << L_TIMESTAMP_MINUTE_BIT_OFFSET; + long localOffsetValue = L_TIMESTAMP_UNKNOWN_OFFSET_VALUE; + if (value.getLocalOffset() != null) { + localOffsetValue = value.getLocalOffset() + (24 * 60); + } + bits |= localOffsetValue << L_TIMESTAMP_OFFSET_BIT_OFFSET; + + if (value.getPrecision() == Timestamp.Precision.MINUTE) { + buffer.writeFlexUInt(6); + buffer.writeFixedUInt(bits, 6); + return 8; // OpCode + FlexUInt + 6 bytes data + } + + + bits |= ((long) value.getSecond()) << L_TIMESTAMP_SECOND_BIT_OFFSET; + int secondsScale = value.getDecimalSecond().scale(); + if (secondsScale == 0) { + buffer.writeFlexUInt(7); + buffer.writeFixedUInt(bits, 7); + return 9; // OpCode + FlexUInt + 7 bytes data + } + + BigDecimal fractionalSeconds = value.getDecimalSecond().remainder(BigDecimal.ONE); + BigInteger coefficient = fractionalSeconds.unscaledValue(); + long exponent = fractionalSeconds.scale(); + int numCoefficientBytes = WriteBuffer.flexUIntLength(coefficient); + int numExponentBytes = WriteBuffer.fixedUIntLength(exponent); + // Years-seconds data (7 bytes) + fraction coefficient + fraction exponent + int dataLength = 7 + numCoefficientBytes + numExponentBytes; + + buffer.writeFlexUInt(dataLength); + buffer.writeFixedUInt(bits, 7); + buffer.writeFlexUInt(coefficient); + buffer.writeFixedUInt(exponent); + + // OpCode + FlexUInt length + dataLength + return 1 + WriteBuffer.flexUIntLength(dataLength) + dataLength; + } + } diff --git a/src/com/amazon/ion/impl/bin/Ion_1_1_Constants.java b/src/com/amazon/ion/impl/bin/Ion_1_1_Constants.java new file mode 100644 index 0000000000..e0c031ee93 --- /dev/null +++ b/src/com/amazon/ion/impl/bin/Ion_1_1_Constants.java @@ -0,0 +1,36 @@ +package com.amazon.ion.impl.bin; + +/** + * Contains constants (other than OpCodes) which are generally applicable to both reading and writing binary Ion 1.1 + */ +public class Ion_1_1_Constants { + private Ion_1_1_Constants() {} + + //////// Timestamp Field Constants //////// + + // S_TIMESTAMP_* is applicable to all short-form timestamps + static final int S_TIMESTAMP_MONTH_BIT_OFFSET = 7; + static final int S_TIMESTAMP_DAY_BIT_OFFSET = 11; + static final int S_TIMESTAMP_HOUR_BIT_OFFSET = 16; + static final int S_TIMESTAMP_MINUTE_BIT_OFFSET = 21; + // S_U_TIMESTAMP_* is applicable to all short-form timestamps with a `U` bit + static final int S_U_TIMESTAMP_UTC_FLAG = 1 << 27; + static final int S_U_TIMESTAMP_SECOND_BIT_OFFSET = 28; + static final int S_U_TIMESTAMP_FRACTION_BIT_OFFSET = 34; + // S_O_TIMESTAMP_* is applicable to all short-form timestamps with `o` (offset) bits + static final int S_O_TIMESTAMP_OFFSET_BIT_OFFSET = 27; + static final int S_O_TIMESTAMP_SECOND_BIT_OFFSET = 34; + + // L_TIMESTAMP_* is applicable to all long-form timestamps + static final int L_TIMESTAMP_MONTH_BIT_OFFSET = 14; + static final int L_TIMESTAMP_DAY_BIT_OFFSET = 18; + static final int L_TIMESTAMP_HOUR_BIT_OFFSET = 23; + static final int L_TIMESTAMP_MINUTE_BIT_OFFSET = 28; + static final int L_TIMESTAMP_OFFSET_BIT_OFFSET = 34; + static final int L_TIMESTAMP_SECOND_BIT_OFFSET = 44; + static final int L_TIMESTAMP_UNKNOWN_OFFSET_VALUE = 0b111111111111; + + //////// Bit masks //////// + + static final long LEAST_SIGNIFICANT_7_BITS = 0b01111111L; +} diff --git a/src/com/amazon/ion/impl/bin/OpCodes.java b/src/com/amazon/ion/impl/bin/OpCodes.java index 0ea5bf843d..4e3f3c6468 100644 --- a/src/com/amazon/ion/impl/bin/OpCodes.java +++ b/src/com/amazon/ion/impl/bin/OpCodes.java @@ -20,9 +20,24 @@ private OpCodes() {} // 0x61-0x6E are additional lengths of decimals. public static final byte NEGATIVE_ZERO_DECIMAL = 0x6F; + public static final byte TIMESTAMP_YEAR_PRECISION = 0x70; + public static final byte TIMESTAMP_MONTH_PRECISION = 0x71; + public static final byte TIMESTAMP_DAY_PRECISION = 0x72; + public static final byte TIMESTAMP_MINUTE_PRECISION = 0x73; + public static final byte TIMESTAMP_SECOND_PRECISION = 0x74; + public static final byte TIMESTAMP_MILLIS_PRECISION = 0x75; + public static final byte TIMESTAMP_MICROS_PRECISION = 0x76; + public static final byte TIMESTAMP_NANOS_PRECISION = 0x77; + public static final byte TIMESTAMP_MINUTE_PRECISION_WITH_OFFSET = 0x78; + public static final byte TIMESTAMP_SECOND_PRECISION_WITH_OFFSET = 0x79; + public static final byte TIMESTAMP_MILLIS_PRECISION_WITH_OFFSET = 0x7A; + public static final byte TIMESTAMP_MICROS_PRECISION_WITH_OFFSET = 0x7B; + public static final byte TIMESTAMP_NANOS_PRECISION_WITH_OFFSET = 0x7C; + // 0x7D-0x7F Reserved public static final byte NULL_UNTYPED = (byte) 0xEA; public static final byte NULL_TYPED = (byte) 0xEB; public static final byte VARIABLE_LENGTH_INTEGER = (byte) 0xF5; + public static final byte VARIABLE_LENGTH_TIMESTAMP = (byte) 0xF7; } diff --git a/src/com/amazon/ion/impl/bin/WriteBuffer.java b/src/com/amazon/ion/impl/bin/WriteBuffer.java index 5a493e8ca0..42305990fb 100644 --- a/src/com/amazon/ion/impl/bin/WriteBuffer.java +++ b/src/com/amazon/ion/impl/bin/WriteBuffer.java @@ -157,6 +157,27 @@ private void writeBytesSlow(final byte[] bytes, int off, int len) } + /** + * Adds all-zero bytes. Useful for padding a FixedInt where the minimum needed to represent the value is + * less than the required number of bytes in the spec. + */ + private void writeZeroBytes(int len) { + if (len < remaining()) { + current.limit += len; + } else do { + final int amount = Math.min(len, current.remaining()); + current.limit += amount; + len -= amount; + if (current.remaining() == 0) { + if (index == blocks.size() - 1) { + allocateNewBlock(); + } + index++; + current = blocks.get(index); + } + } while (len > 0); + } + /** Writes an array of bytes to the buffer expanding if necessary. */ public void writeBytes(final byte[] bytes, final int off, final int len) { @@ -1453,17 +1474,39 @@ public static int fixedUIntLength(final long value) { */ public int writeFixedUInt(final long value) { if (value < 0) { - throw new IllegalArgumentException("Attempted to write a FlexUInt for " + value); + throw new IllegalArgumentException("Attempted to write a FixedUInt for " + value); } int numBytes = fixedUIntLength(value); return writeFixedIntOrUInt(value, numBytes); } /** - * Because the fixed int and fixed uint encodings are so similar, we can use this method to write either one as long - * as we provide the correct number of bytes needed to encode the value. + * Writes a fixed int with the specified number of bytes. + * Be careful. If numBytes is lower than the minimum number of bytes needed to represent that number, + * you will lose data. + * This is useful for cases where the spec requires a fixed int of a specific size, + */ + public int writeFixedUInt(final long value, final int numBytes) { + return writeFixedIntOrUInt(value, numBytes); + } + + /** + * Writes the bytes of a {@code long} as a {@code FixedInt} or {@code FixedUInt} using {@code numBytes} bytes. + *

+ * {@code numBytes} should be an integer from 1 to 8 inclusive. Out of bounds integers will have the same effect + * as the nearest valid integer. + *

+ * Because the {@code FixedInt} and {@code FixedUInt} encodings are so similar, we can use this method to write + * either one as long as we provide the correct number of bytes needed to encode the value. + *

+ * Most of the time, you should not call this method directly. Instead, use {@link WriteBuffer#writeFixedInt} or + * {@link WriteBuffer#writeFixedUInt}, which calculate the minimum number of required bytes and then delegate to + * this method. + *

+ * You should use this method when the spec requires a {@code FixedInt} or {@code FixedUInt} of a specific + * size when it's possible that the size of the data could be smaller than the specified size. */ - private int writeFixedIntOrUInt(final long value, final int numBytes) { + public int writeFixedIntOrUInt(final long value, final int numBytes) { writeByte((byte) value); if (numBytes > 1) { writeByte((byte) (value >> 8)); 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 4d26345d77..0916e70b9c 100644 --- a/test/com/amazon/ion/impl/bin/IonEncoder_1_1Test.java +++ b/test/com/amazon/ion/impl/bin/IonEncoder_1_1Test.java @@ -1,10 +1,14 @@ package com.amazon.ion.impl.bin; import com.amazon.ion.IonType; +import com.amazon.ion.Timestamp; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.converter.ArgumentConversionException; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.converter.TypedArgumentConverter; import org.junit.jupiter.params.provider.CsvSource; import java.io.ByteArrayOutputStream; @@ -33,8 +37,8 @@ private byte[] bytes() { } /** - * Checks that the function writes the expected bytes and returns the expected - * count of written bytes for the given input value. + * Checks that the function writes the expected bytes and returns the expected count of written bytes for the + * given input value. The expected bytes should be a string of space-separated hexadecimal pairs. */ private void assertWritingValue(String expectedBytes, T value, BiFunction writeOperation) { int numBytes = writeOperation.apply(buf, value); @@ -42,6 +46,16 @@ private void assertWritingValue(String expectedBytes, T value, BiFunction void assertWritingValueWithBinary(String expectedBytes, T value, BiFunction writeOperation) { + int numBytes = writeOperation.apply(buf, value); + Assertions.assertEquals(expectedBytes, byteArrayToBitString(bytes())); + Assertions.assertEquals(byteLengthFromBitString(expectedBytes), numBytes); + } + @ParameterizedTest @CsvSource({ " NULL, EA", @@ -72,7 +86,7 @@ public void testWriteNullValueForDatagram() { "true, 5E", "false, 5F", }) - public void testWriteBooleanValue(Boolean value, String expectedBytes) { + public void testWriteBooleanValue(boolean value, String expectedBytes) { assertWritingValue(expectedBytes, value, IonEncoder_1_1::writeBoolValue); } @@ -144,8 +158,8 @@ public void testWriteIntegerValue(long value, String expectedBytes) { " -9223372036854775809, F5 13 FF FF FF FF FF FF FF 7F FF", "-99999999999999999999999999999, F5 1B 01 00 00 60 35 E8 8D 92 51 F0 E1 BC FE", }) - public void testWriteIntegerValueForBigInteger(String value, String expectedBytes) { - assertWritingValue(expectedBytes, new BigInteger(value), IonEncoder_1_1::writeIntValue); + public void testWriteIntegerValueForBigInteger(BigInteger value, String expectedBytes) { + assertWritingValue(expectedBytes, value, IonEncoder_1_1::writeIntValue); } @Test @@ -205,6 +219,186 @@ public void testWriteFloatValueForDouble(double value, String expectedBytes) { assertWritingValue(expectedBytes, value, IonEncoder_1_1::writeFloat); } + // Because timestamp subfields are smeared across bytes, it's easier to reason about them in 1s and 0s + // instead of hex digits + @ParameterizedTest + @CsvSource({ + // OpCode MYYYYYYY DDDDDMMM mmmHHHHH ssssUmmm ffffffss ffffffff ffffffff ffffffff + "2023-10-15T01:00Z, 01110011 00110101 01111101 00000001 00001000", + "2023-10-15T01:59Z, 01110011 00110101 01111101 01100001 00001111", + "2023-10-15T11:22Z, 01110011 00110101 01111101 11001011 00001010", + "2023-10-15T23:00Z, 01110011 00110101 01111101 00010111 00001000", + "2023-10-15T23:59Z, 01110011 00110101 01111101 01110111 00001111", + "2023-10-15T11:22:00Z, 01110100 00110101 01111101 11001011 00001010 00000000", + "2023-10-15T11:22:33Z, 01110100 00110101 01111101 11001011 00011010 00000010", + "2023-10-15T11:22:59Z, 01110100 00110101 01111101 11001011 10111010 00000011", + "2023-10-15T11:22:33.000Z, 01110101 00110101 01111101 11001011 00011010 00000010 00000000", + "2023-10-15T11:22:33.444Z, 01110101 00110101 01111101 11001011 00011010 11110010 00000110", + "2023-10-15T11:22:33.999Z, 01110101 00110101 01111101 11001011 00011010 10011110 00001111", + "2023-10-15T11:22:33.000000Z, 01110110 00110101 01111101 11001011 00011010 00000010 00000000 00000000", + "2023-10-15T11:22:33.444555Z, 01110110 00110101 01111101 11001011 00011010 00101110 00100010 00011011", + "2023-10-15T11:22:33.999999Z, 01110110 00110101 01111101 11001011 00011010 11111110 00001000 00111101", + "2023-10-15T11:22:33.000000000Z, 01110111 00110101 01111101 11001011 00011010 00000010 00000000 00000000 00000000", + "2023-10-15T11:22:33.444555666Z, 01110111 00110101 01111101 11001011 00011010 01001010 10000110 11111101 01101001", + "2023-10-15T11:22:33.999999999Z, 01110111 00110101 01111101 11001011 00011010 11111110 00100111 01101011 11101110", + }) + public void testWriteTimestampValueWithUtcShortForm(@ConvertWith(StringToTimestamp.class) Timestamp value, String expectedBytes) { + assertWritingValueWithBinary(expectedBytes, value, IonEncoder_1_1::writeTimestampValue); + } + + + @ParameterizedTest + @CsvSource({ + // OpCode MYYYYYYY DDDDDMMM mmmHHHHH ssssUmmm ffffffss ffffffff ffffffff ffffffff + "1970T, 01110000 00000000", + "2023T, 01110000 00110101", + "2097T, 01110000 01111111", + "2023-01T, 01110001 10110101 00000000", + "2023-10T, 01110001 00110101 00000101", + "2023-12T, 01110001 00110101 00000110", + "2023-10-01T, 01110010 00110101 00001101", + "2023-10-15T, 01110010 00110101 01111101", + "2023-10-31T, 01110010 00110101 11111101", + "2023-10-15T01:00-00:00, 01110011 00110101 01111101 00000001 00000000", + "2023-10-15T01:59-00:00, 01110011 00110101 01111101 01100001 00000111", + "2023-10-15T11:22-00:00, 01110011 00110101 01111101 11001011 00000010", + "2023-10-15T23:00-00:00, 01110011 00110101 01111101 00010111 00000000", + "2023-10-15T23:59-00:00, 01110011 00110101 01111101 01110111 00000111", + "2023-10-15T11:22:00-00:00, 01110100 00110101 01111101 11001011 00000010 00000000", + "2023-10-15T11:22:33-00:00, 01110100 00110101 01111101 11001011 00010010 00000010", + "2023-10-15T11:22:59-00:00, 01110100 00110101 01111101 11001011 10110010 00000011", + "2023-10-15T11:22:33.000-00:00, 01110101 00110101 01111101 11001011 00010010 00000010 00000000", + "2023-10-15T11:22:33.444-00:00, 01110101 00110101 01111101 11001011 00010010 11110010 00000110", + "2023-10-15T11:22:33.999-00:00, 01110101 00110101 01111101 11001011 00010010 10011110 00001111", + "2023-10-15T11:22:33.000000-00:00, 01110110 00110101 01111101 11001011 00010010 00000010 00000000 00000000", + "2023-10-15T11:22:33.444555-00:00, 01110110 00110101 01111101 11001011 00010010 00101110 00100010 00011011", + "2023-10-15T11:22:33.999999-00:00, 01110110 00110101 01111101 11001011 00010010 11111110 00001000 00111101", + "2023-10-15T11:22:33.000000000-00:00, 01110111 00110101 01111101 11001011 00010010 00000010 00000000 00000000 00000000", + "2023-10-15T11:22:33.444555666-00:00, 01110111 00110101 01111101 11001011 00010010 01001010 10000110 11111101 01101001", + "2023-10-15T11:22:33.999999999-00:00, 01110111 00110101 01111101 11001011 00010010 11111110 00100111 01101011 11101110", + }) + public void testWriteTimestampValueWithUnknownOffsetShortForm(@ConvertWith(StringToTimestamp.class) Timestamp value, String expectedBytes) { + assertWritingValueWithBinary(expectedBytes, value, IonEncoder_1_1::writeTimestampValue); + } + + @ParameterizedTest + @CsvSource({ + // OpCode MYYYYYYY DDDDDMMM mmmHHHHH ooooommm ssssssoo ffffffff ffffffff ffffffff ..ffffff + "2023-10-15T01:00-14:00, 01111000 00110101 01111101 00000001 01000000 00000010", + "2023-10-15T01:00+14:00, 01111000 00110101 01111101 00000001 11000000 00000001", + "2023-10-15T01:00-01:15, 01111000 00110101 01111101 00000001 11011000 00000011", + "2023-10-15T01:00+01:15, 01111000 00110101 01111101 00000001 00101000 00000000", + "2023-10-15T01:59+01:15, 01111000 00110101 01111101 01100001 00101111 00000000", + "2023-10-15T11:22+01:15, 01111000 00110101 01111101 11001011 00101010 00000000", + "2023-10-15T23:00+01:15, 01111000 00110101 01111101 00010111 00101000 00000000", + "2023-10-15T23:59+01:15, 01111000 00110101 01111101 01110111 00101111 00000000", + "2023-10-15T11:22:00+01:15, 01111001 00110101 01111101 11001011 00101010 00000000", + "2023-10-15T11:22:33+01:15, 01111001 00110101 01111101 11001011 00101010 10000100", + "2023-10-15T11:22:59+01:15, 01111001 00110101 01111101 11001011 00101010 11101100", + "2023-10-15T11:22:33.000+01:15, 01111010 00110101 01111101 11001011 00101010 10000100 00000000 00000000", + "2023-10-15T11:22:33.444+01:15, 01111010 00110101 01111101 11001011 00101010 10000100 10111100 00000001", + "2023-10-15T11:22:33.999+01:15, 01111010 00110101 01111101 11001011 00101010 10000100 11100111 00000011", + "2023-10-15T11:22:33.000000+01:15, 01111011 00110101 01111101 11001011 00101010 10000100 00000000 00000000 00000000", + "2023-10-15T11:22:33.444555+01:15, 01111011 00110101 01111101 11001011 00101010 10000100 10001011 11001000 00000110", + "2023-10-15T11:22:33.999999+01:15, 01111011 00110101 01111101 11001011 00101010 10000100 00111111 01000010 00001111", + "2023-10-15T11:22:33.000000000+01:15, 01111100 00110101 01111101 11001011 00101010 10000100 00000000 00000000 00000000 00000000", + "2023-10-15T11:22:33.444555666+01:15, 01111100 00110101 01111101 11001011 00101010 10000100 10010010 01100001 01111111 00011010", + "2023-10-15T11:22:33.999999999+01:15, 01111100 00110101 01111101 11001011 00101010 10000100 11111111 11001001 10011010 00111011", + + }) + public void testWriteTimestampValueWithKnownOffsetShortForm(@ConvertWith(StringToTimestamp.class) Timestamp value, String expectedBytes) { + assertWritingValueWithBinary(expectedBytes, value, IonEncoder_1_1::writeTimestampValue); + } + + @ParameterizedTest + @CsvSource({ + // OpCode Length YYYYYYYY MMYYYYYY HDDDDDMM mmmmHHHH oooooomm ssoooooo ....ssss Coefficient+ Scale + "0001T, 11110111 00000101 00000001 00000000", + "1947T, 11110111 00000101 10011011 00000111", + "9999T, 11110111 00000101 00001111 00100111", + "1947-01T, 11110111 00000111 10011011 01000111 00000000", + "1947-12T, 11110111 00000111 10011011 00000111 00000011", + "1947-01-01T, 11110111 00000111 10011011 01000111 00000100", + "1947-12-23T, 11110111 00000111 10011011 00000111 01011111", + "1947-12-31T, 11110111 00000111 10011011 00000111 01111111", + "1947-12-23T00:00Z, 11110111 00001101 10011011 00000111 01011111 00000000 10000000 00010110", + "1947-12-23T23:59Z, 11110111 00001101 10011011 00000111 11011111 10111011 10000011 00010110", + "1947-12-23T23:59:00Z, 11110111 00001111 10011011 00000111 11011111 10111011 10000011 00010110 00000000", + "1947-12-23T23:59:59Z, 11110111 00001111 10011011 00000111 11011111 10111011 10000011 10110110 00000011", + "1947-12-23T23:59:00.0Z, 11110111 00010011 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00000001 00000001", + "1947-12-23T23:59:00.00Z, 11110111 00010011 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00000001 00000010", + "1947-12-23T23:59:00.000Z, 11110111 00010011 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00000001 00000011", + "1947-12-23T23:59:00.0000Z, 11110111 00010011 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00000001 00000100", + "1947-12-23T23:59:00.00000Z, 11110111 00010011 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00000001 00000101", + "1947-12-23T23:59:00.000000Z, 11110111 00010011 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00000001 00000110", + "1947-12-23T23:59:00.0000000Z, 11110111 00010011 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00000001 00000111", + "1947-12-23T23:59:00.00000000Z, 11110111 00010011 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00000001 00001000", + "1947-12-23T23:59:00.9Z, 11110111 00010011 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00010011 00000001", + "1947-12-23T23:59:00.99Z, 11110111 00010011 10011011 00000111 11011111 10111011 10000011 00010110 00000000 11000111 00000010", + "1947-12-23T23:59:00.999Z, 11110111 00010101 10011011 00000111 11011111 10111011 10000011 00010110 00000000 10011110 00001111 00000011", + "1947-12-23T23:59:00.9999Z, 11110111 00010101 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00111110 10011100 00000100", + "1947-12-23T23:59:00.99999Z, 11110111 00010111 10011011 00000111 11011111 10111011 10000011 00010110 00000000 11111100 00110100 00001100 00000101", + "1947-12-23T23:59:00.999999Z, 11110111 00010111 10011011 00000111 11011111 10111011 10000011 00010110 00000000 11111100 00010001 01111010 00000110", + "1947-12-23T23:59:00.9999999Z, 11110111 00011001 10011011 00000111 11011111 10111011 10000011 00010110 00000000 11111000 01100111 10001001 00001001 00000111", + "1947-12-23T23:59:00.99999999Z, 11110111 00011001 10011011 00000111 11011111 10111011 10000011 00010110 00000000 11111000 00001111 01011110 01011111 00001000", + + "1947-12-23T23:59:00.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000Z, " + + "11110111 00010011 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00000001 10001101", + + "1947-12-23T23:59:00.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000Z, " + + "11110111 00010101 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00000001 01101000 00000001", + + "1947-12-23T23:59:00.999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999Z, " + + "11110111 10010111 10011011 00000111 11011111 10111011 10000011 00010110 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 " + + "11111100 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 " + + "11111111 10010100 10001001 01111001 01101100 11001110 01111000 11110010 01000000 01111101 10100110 11000111 10101000 01000110 01011001 01110001 01001101 " + + "00100000 11110101 01101110 01111010 00001100 00001001 11101111 01111111 11110011 00011110 00010100 11010111 01101000 01110111 10101100 01101100 10001110 " + + "00110010 10110111 10000010 11110010 00110110 01101000 11110010 10100111 10001101", + + + // Offsets + "2048-01-01T01:01-23:59, 11110111 00001101 00000000 01001000 10000100 00010000 00000100 00000000", + "2048-01-01T01:01-00:02, 11110111 00001101 00000000 01001000 10000100 00010000 01111000 00010110", + "2048-01-01T01:01-00:01, 11110111 00001101 00000000 01001000 10000100 00010000 01111100 00010110", + "2048-01-01T01:01-00:00, 11110111 00001101 00000000 01001000 10000100 00010000 11111100 00111111", + "2048-01-01T01:01+00:00, 11110111 00001101 00000000 01001000 10000100 00010000 10000000 00010110", + "2048-01-01T01:01+00:01, 11110111 00001101 00000000 01001000 10000100 00010000 10000100 00010110", + "2048-01-01T01:01+00:02, 11110111 00001101 00000000 01001000 10000100 00010000 10001000 00010110", + "2048-01-01T01:01+23:59, 11110111 00001101 00000000 01001000 10000100 00010000 11111100 00101100", + }) + public void testWriteTimestampValueLongForm(@ConvertWith(StringToTimestamp.class) Timestamp value, String expectedBytes) { + assertWritingValueWithBinary(expectedBytes, value, IonEncoder_1_1::writeLongFormTimestampValue); + } + + @ParameterizedTest + @CsvSource({ + // Long form because it's out of the year range + "0001T, 11110111 00000101 00000001 00000000", + "9999T, 11110111 00000101 00001111 00100111", + + // Long form because the offset is too high/low + "2048-01-01T01:01+14:15, 11110111 00001101 00000000 01001000 10000100 00010000 11011100 00100011", + "2048-01-01T01:01-14:15, 11110111 00001101 00000000 01001000 10000100 00010000 00100100 00001001", + + // Long form because the offset is not a multiple of 15 + "2048-01-01T01:01+00:01, 11110111 00001101 00000000 01001000 10000100 00010000 10000100 00010110", + + // Long form because the fractional seconds are millis, micros, or nanos + "2023-12-31T23:59:00.0Z, 11110111 00010011 11100111 00000111 11111111 10111011 10000011 00010110 00000000 00000001 00000001", + }) + public void testWriteTimestampDelegatesCorrectlyToLongForm(@ConvertWith(StringToTimestamp.class) Timestamp value, String expectedBytes) { + assertWritingValueWithBinary(expectedBytes, value, IonEncoder_1_1::writeTimestampValue); + } + + @Test + public void testWriteTimestampValueForNullTimestamp() { + int numBytes = IonEncoder_1_1.writeTimestampValue(buf, null); + Assertions.assertEquals("EB 04", byteArrayToHex(bytes())); + Assertions.assertEquals(2, numBytes); + } + /** * Utility method to make it easier to write test cases that assert specific sequences of bytes. */ @@ -222,4 +416,45 @@ private static String byteArrayToHex(byte[] bytes) { private static int byteLengthFromHexString(String hexString) { return (hexString.replaceAll("[^\\dA-F]", "").length() - 1) / 2 + 1; } + + /** + * Converts a byte array to a string of bits, such as "00110110 10001001". + * The purpose of this method is to make it easier to read and write test assertions. + */ + private static String byteArrayToBitString(byte[] bytes) { + StringBuilder s = new StringBuilder(); + for (byte aByte : bytes) { + for (int bit = 7; bit >= 0; bit--) { + if (((0x01 << bit) & aByte) != 0) { + s.append("1"); + } else { + s.append("0"); + } + } + s.append(" "); + } + return s.toString().trim(); + } + + /** + * Determines the number of bytes needed to represent a series of hexadecimal digits. + */ + private static int byteLengthFromBitString(String bitString) { + return (bitString.replaceAll("[^01]", "").length() - 1) / 8 + 1; + } + + /** + * Converts a String to a Timestamp for a @Parameterized test + */ + static class StringToTimestamp extends TypedArgumentConverter { + protected StringToTimestamp() { + super(String.class, Timestamp.class); + } + + @Override + protected Timestamp convert(String source) throws ArgumentConversionException { + if (source == null) return null; + return Timestamp.valueOf(source); + } + } } diff --git a/test/com/amazon/ion/impl/bin/WriteBufferTest.java b/test/com/amazon/ion/impl/bin/WriteBufferTest.java index a46e4c2c75..e062f3d80a 100644 --- a/test/com/amazon/ion/impl/bin/WriteBufferTest.java +++ b/test/com/amazon/ion/impl/bin/WriteBufferTest.java @@ -1615,6 +1615,36 @@ public void testWriteFixedUInt(long value, String expectedBits) { Assertions.assertEquals((expectedBits.length() + 1)/9, numBytes); } + @ParameterizedTest + @CsvSource({ + " 0, 1, 00000000", + " 0, 2, 00000000 00000000", + " 0, 8, 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000", + " 1, 1, 00000001", + " 1, 2, 00000001 00000000", + " 1, 8, 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000", + " 255, 1, 11111111", + " 255, 2, 11111111 00000000", + " 255, 3, 11111111 00000000 00000000", + " -1, 1, 11111111", + " -1, 2, 11111111 11111111", + " -1, 8, 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111", + // Long.MIN_VALUE and Long.MAX_VALUE + " 9223372036854775807, 8, 11111111 11111111 11111111 11111111 11111111 11111111 11111111 01111111", + "-9223372036854775808, 8, 00000000 00000000 00000000 00000000 00000000 00000000 00000000 10000000", + // Out of bounds values for `numBytes` + " 0, 0, 00000000", + " -1, 0, 11111111", + " 0, 9, 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000", + " -1, 9, 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111", + }) + public void testWriteFixedIntOrUInt(long value, int numBytes, String expectedBits) { + int actualNumBytes = buf.writeFixedIntOrUInt(value, numBytes); + String actualBits = byteArrayToBitString(bytes()); + Assertions.assertEquals(expectedBits, actualBits); + Assertions.assertEquals(numBytes, actualNumBytes); + } + @Test public void testWriteFixedUIntForNegativeNumber() { Assertions.assertThrows(IllegalArgumentException.class, () -> buf.writeFixedUInt(-1));