diff --git a/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/codecs/DateTimeCodec.java b/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/codecs/DateTimeCodec.java index 110569f..1044a59 100644 --- a/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/codecs/DateTimeCodec.java +++ b/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/codecs/DateTimeCodec.java @@ -14,6 +14,30 @@ import java.time.ZoneOffset; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +/** + * Provides static methods for encoding and decoding {@link OffsetDateTime}s serialized as {@code System.DateTime}s + * + * {@link OffsetDateTime} values are serialized as unsigned 64-bit integers: + * + * + * + * + * + * + *
+ * Bits 01-62 + * + * Contain the number of 100-nanosecond ticks where 0 represents {@code 1/1/0001 12:00am}, up until the value + * {@code 12/31/9999 23:59:59.9999999}. + *
+ * Bits 63-64 + * + * Contain a four-state value that describes the {@code System.DateTimeKind} value of the date time, with a + * 2nd value for the rare case where the date time is local, but is in an overlapped daylight savings time + * hour and it is in daylight savings time. This allows distinction of these otherwise ambiguous local times + * and prevents data loss when round tripping from Local to UTC time. + *
+ */ public final class DateTimeCodec { public static final int BYTES = Long.BYTES; @@ -24,6 +48,8 @@ public final class DateTimeCodec { private static final long KIND_UTC = 0x4000000000000000L; private static final long TICKS_MASK = 0x3FFFFFFFFFFFFFFFL; + private static final long UNIX_EPOCH_TICKS = 0x89F7FF5F7B58000L; + private static final ZoneOffset ZONE_OFFSET_LOCAL = OffsetDateTime.now().getOffset(); private static final int ZONE_OFFSET_LOCAL_TOTAL_SECONDS = ZONE_OFFSET_LOCAL.getTotalSeconds(); private static final int ZONE_OFFSET_UTC_TOTAL_SECONDS = ZoneOffset.UTC.getTotalSeconds(); @@ -61,10 +87,13 @@ public final class DateTimeCodec { in.readableBytes()); final long data = in.readLongLE(); - final long epochSecond = data & TICKS_MASK; + final long ticks = data & TICKS_MASK; final ZoneOffset zoneOffset = (data & FLAGS_MASK) == KIND_UTC ? ZoneOffset.UTC : ZONE_OFFSET_LOCAL; - return OffsetDateTime.ofInstant(Instant.ofEpochSecond(epochSecond), zoneOffset); + final long epochSecond = ((ticks - UNIX_EPOCH_TICKS) / 10_000_000L) - zoneOffset.getTotalSeconds(); + final int nanos = (int) (100L * (ticks % 10_000_000L)); + + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(epochSecond, nanos), zoneOffset); } /** @@ -77,7 +106,7 @@ public final class DateTimeCodec { */ public static byte[] encode(final OffsetDateTime offsetDateTime) { final byte[] bytes = new byte[BYTES]; - encode(offsetDateTime, Unpooled.wrappedBuffer(bytes)); + encode(offsetDateTime, Unpooled.wrappedBuffer(bytes).clear()); return bytes; } @@ -91,21 +120,25 @@ public final class DateTimeCodec { */ public static void encode(final OffsetDateTime offsetDateTime, final ByteBuf out) { - final long epochSecond = offsetDateTime.toEpochSecond(); + final ZoneOffset offset = offsetDateTime.getOffset(); + final Instant instant = offsetDateTime.toInstant(); - checkArgument(epochSecond <= TICKS_MASK, "expected offsetDateTime epoch second in range [0, %s], not %s", + final long ticks = UNIX_EPOCH_TICKS + 10_000_000L * (instant.getEpochSecond() + offset.getTotalSeconds()) + + instant.getNano() / 100L; + + checkArgument(ticks <= TICKS_MASK, "expected offsetDateTime epoch second in range [0, %s], not %s", TICKS_MASK, - epochSecond); + ticks); final int zoneOffsetTotalSeconds = offsetDateTime.getOffset().getTotalSeconds(); final long value; if (zoneOffsetTotalSeconds == ZONE_OFFSET_UTC_TOTAL_SECONDS) { - value = epochSecond | KIND_UTC; + value = ticks | KIND_UTC; } else if (zoneOffsetTotalSeconds == ZONE_OFFSET_LOCAL_TOTAL_SECONDS) { - value = epochSecond | KIND_LOCAL; + value = ticks | KIND_LOCAL; } else { - value = epochSecond | KIND_AMBIGUOUS; + value = ticks | KIND_AMBIGUOUS; } out.writeLongLE(value); diff --git a/java/src/test/java/com/azure/data/cosmos/serialization/hybridrow/codecs/DateTimeCodecTest.java b/java/src/test/java/com/azure/data/cosmos/serialization/hybridrow/codecs/DateTimeCodecTest.java index 935a1ba..e80a0b8 100644 --- a/java/src/test/java/com/azure/data/cosmos/serialization/hybridrow/codecs/DateTimeCodecTest.java +++ b/java/src/test/java/com/azure/data/cosmos/serialization/hybridrow/codecs/DateTimeCodecTest.java @@ -35,7 +35,7 @@ public class DateTimeCodecTest { @Test(dataProvider = "dateTimeDataProvider") public void testDecodeByteBuf(byte[] buffer, OffsetDateTime value) { - ByteBuf byteBuf = Unpooled.wrappedBuffer(new byte[DateTimeCodec.BYTES]); + ByteBuf byteBuf = Unpooled.wrappedBuffer(buffer); OffsetDateTime actual = DateTimeCodec.decode(byteBuf); assertEquals(actual, value); } @@ -48,13 +48,14 @@ public class DateTimeCodecTest { @Test(dataProvider = "dateTimeDataProvider") public void testEncodeByteBuf(byte[] buffer, OffsetDateTime value) { - ByteBuf actual = Unpooled.wrappedBuffer(new byte[DateTimeCodec.BYTES]); + ByteBuf actual = Unpooled.wrappedBuffer(new byte[DateTimeCodec.BYTES]).clear(); DateTimeCodec.encode(value, actual); assertEquals(actual.array(), buffer); } @DataProvider(name = "dateTimeDataProvider") - private Iterator dateTimeData(byte[] buffer, OffsetDateTime value) { + private static Iterator dateTimeData() { + ImmutableList items = ImmutableList.of( new DateTimeItem(new byte[] { (byte) 120, (byte) 212, (byte) 106, (byte) 251, (byte) 105, (byte) 48, (byte) 215, (byte) 136 }, @@ -63,6 +64,7 @@ public class DateTimeCodecTest { (byte) 226, (byte) 108, (byte) 87, (byte) 194, (byte) 164, (byte) 48, (byte) 215, (byte) 72 }, OffsetDateTime.parse("2019-09-03T19:27:28.9493730Z")) ); + return items.stream().map(item -> new Object[] { item.buffer, item.value }).iterator(); }