diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/DateTimeUtilsTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/DateTimeUtilsTest.java index 056aa7576b1e..b03c54e68299 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/DateTimeUtilsTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/DateTimeUtilsTest.java @@ -28,9 +28,9 @@ import org.junit.Ignore; import org.junit.Test; +import java.time.Instant; import java.time.ZoneId; import java.time.ZoneOffset; -import java.time.ZonedDateTime; import java.util.TimeZone; import static org.junit.Assert.assertEquals; @@ -48,7 +48,7 @@ public class DateTimeUtilsTest { /** Test convertDatetimeStrToLong() method with different time precision. */ @Test public void convertDatetimeStrToLongTest1() { - zoneOffset = ZonedDateTime.now().getOffset(); + zoneOffset = Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).getOffset(); zoneId = ZoneId.systemDefault(); if (zoneOffset.toString().equals("Z")) { delta = 8 * 3600000; @@ -84,11 +84,12 @@ public void convertDatetimeStrToLongTest3() { public void convertDatetimeStrToLongTest4() { zoneOffset = ZoneOffset.UTC; try { - DateTimeUtils.convertDatetimeStrToLong("1999-02-29T00:00:00.000", zoneOffset, 0, "ms"); + DateTimeUtils.convertDatetimeStrToLong( + "1999-02-29T00:00:00.000", (ZoneId) zoneOffset, 0, "ms"); fail(); } catch (Exception e) { assertEquals( - "Text '1999-02-29T00:00:00.000+00:00' could not be parsed: Invalid date 'February 29' as '1999' is not a leap year", + "Text '1999-02-29T00:00:00.000' could not be parsed: Invalid date 'February 29' as '1999' is not a leap year", e.getMessage()); } } @@ -258,7 +259,8 @@ public void testConvertDatetimeStrToLongWithoutMS( "2019.01.02T15:13:27" + zoneOffset, }; for (String str : timeFormatWithoutMs) { - Assert.assertEquals(res, DateTimeUtils.convertDatetimeStrToLong(str, zoneOffset, 0, "ms")); + Assert.assertEquals( + res, DateTimeUtils.convertDatetimeStrToLong(str, (ZoneId) zoneOffset, 0, "ms")); } for (String str : timeFormatWithoutMs) { @@ -283,7 +285,7 @@ public void testConvertDatetimeStrToLongWithMS(ZoneOffset zoneOffset, ZoneId zon "2019.01.02T15:13:27.689" + zoneOffset, }; for (String str : timeFormatWithoutMs) { - assertEquals(res, DateTimeUtils.convertDatetimeStrToLong(str, zoneOffset, 0, "ms")); + assertEquals(res, DateTimeUtils.convertDatetimeStrToLong(str, (ZoneId) zoneOffset, 0, "ms")); } for (String str : timeFormatWithoutMs) { @@ -322,7 +324,7 @@ public void testConvertDatetimeStrToLongWithMS2(ZoneOffset zoneOffset, ZoneId zo "2019.01.02T15:13:27.68" + zoneOffset, }; for (String str : timeFormatWithoutMs) { - assertEquals(res, DateTimeUtils.convertDatetimeStrToLong(str, zoneOffset, 0, "ms")); + assertEquals(res, DateTimeUtils.convertDatetimeStrToLong(str, (ZoneId) zoneOffset, 0, "ms")); } for (String str : timeFormatWithoutMs) { @@ -336,7 +338,7 @@ public void testConvertDateStrToLong(ZoneOffset zoneOffset, ZoneId zoneId, long "2019-01-02", "2019/01/02", "2019.01.02", }; for (String str : timeFormatWithoutMs) { - assertEquals(res, DateTimeUtils.convertDatetimeStrToLong(str, zoneOffset, 0, "ms")); + assertEquals(res, DateTimeUtils.convertDatetimeStrToLong(str, (ZoneId) zoneOffset, 0, "ms")); } for (String str : timeFormatWithoutMs) { @@ -373,4 +375,84 @@ public void testConstructTimeDuration() { timeDuration = DataNodeDateTimeUtils.constructTimeDuration("10000000000ms"); Assert.assertEquals(10000000000L, timeDuration.nonMonthDuration); } + + @Test + public void convertWinterTimeShouldUseUtcPlus1() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + long winter = DateTimeUtils.convertDatetimeStrToLong("2024-01-15 12:00:00", zoneId, "ms"); + assertEquals(ZoneOffset.ofHours(1), zoneId.getRules().getOffset(Instant.ofEpochMilli(winter))); + } + + @Test + public void convertSummerTimeShouldUseUtcPlus2() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + long summer = DateTimeUtils.convertDatetimeStrToLong("2024-06-15 12:00:00", zoneId, "ms"); + assertEquals(ZoneOffset.ofHours(2), zoneId.getRules().getOffset(Instant.ofEpochMilli(summer))); + } + + @Test + public void convertJustBeforeSpringDstShouldKeepWinterOffset() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + long before = DateTimeUtils.convertDatetimeStrToLong("2024-03-31 01:59:59", zoneId, "ms"); + assertEquals(ZoneOffset.ofHours(1), zoneId.getRules().getOffset(Instant.ofEpochMilli(before))); + } + + @Test + public void convertJustAfterSpringDstShouldUseSummerOffset() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + long after = DateTimeUtils.convertDatetimeStrToLong("2024-03-31 03:00:00", zoneId, "ms"); + assertEquals(ZoneOffset.ofHours(2), zoneId.getRules().getOffset(Instant.ofEpochMilli(after))); + } + + @Test + public void convertAutumnOverlapShouldResolveToEarlierOffset() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + long overlap = DateTimeUtils.convertDatetimeStrToLong("2024-10-27 02:30:00", zoneId, "ms"); + assertEquals(ZoneOffset.ofHours(2), zoneId.getRules().getOffset(Instant.ofEpochMilli(overlap))); + } + + @Test + public void convertAfterAutumnTransitionShouldUseWinterOffset() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + long after = DateTimeUtils.convertDatetimeStrToLong("2024-10-27 03:00:00", zoneId, "ms"); + assertEquals(ZoneOffset.ofHours(1), zoneId.getRules().getOffset(Instant.ofEpochMilli(after))); + } + + @Test + public void historicalDateBeforeStandardizedOffsetShouldUseLMT() { + ZoneId shanghaiId = ZoneId.of("Asia/Shanghai"); + long oldTime = DateTimeUtils.convertDatetimeStrToLong("1900-01-01 00:00:00", shanghaiId, "ms"); + ZoneOffset offset = shanghaiId.getRules().getOffset(Instant.ofEpochMilli(oldTime)); + assertEquals(8 * 3600 + 5 * 60 + 43, offset.getTotalSeconds()); + } + + @Test + public void explicitOffsetInStringOverridesZoneId() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + long withMs = DateTimeUtils.convertDatetimeStrToLong("2024-06-15 12:00:00Z", zoneId, "ms"); + long withoutMs = DateTimeUtils.convertDatetimeStrToLong("2024-06-15 14:00:00", zoneId, "ms"); + assertEquals(withoutMs, withMs); + + long withUs = + DateTimeUtils.convertDatetimeStrToLong("2024-06-15 12:00:00.123456Z", zoneId, "us"); + long withoutUs = + DateTimeUtils.convertDatetimeStrToLong("2024-06-15 14:00:00.123456", zoneId, "us"); + assertEquals(withoutUs, withUs); + + long withNs = + DateTimeUtils.convertDatetimeStrToLong("2024-06-15 12:00:00.123456789Z", zoneId, "ns"); + long withoutNs = + DateTimeUtils.convertDatetimeStrToLong("2024-06-15 14:00:00.123456789", zoneId, "ns"); + assertEquals(withoutNs, withNs); + } + + @Test + public void springGapShouldShiftForwardInWarsaw() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + long gap = DateTimeUtils.convertDatetimeStrToLong("2024-03-31 02:30:00", zoneId, "ms"); + assertEquals(ZoneOffset.ofHours(2), zoneId.getRules().getOffset(Instant.ofEpochMilli(gap))); + assertEquals( + "2024-03-31T03:30", + Instant.ofEpochMilli(gap).atZone(zoneId).toLocalDateTime().toString().substring(0, 16)); + } } diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/utils/DateTimeUtils.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/utils/DateTimeUtils.java index ccab91ba0a99..7b7cc4b61449 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/utils/DateTimeUtils.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/utils/DateTimeUtils.java @@ -28,6 +28,7 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; @@ -36,6 +37,7 @@ import java.time.format.ResolverStyle; import java.time.format.SignStyle; import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; import java.util.TimeZone; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -479,6 +481,57 @@ public static void initTimestampPrecision() { .toFormatter(); } + private static final DateTimeFormatter ISO_LOCAL_DATE_TIME_WITH_NS_T; + private static final DateTimeFormatter ISO_LOCAL_DATE_TIME_WITH_SLASH_NS_T; + private static final DateTimeFormatter ISO_LOCAL_DATE_TIME_WITH_DOT_NS_T; + private static final DateTimeFormatter ISO_LOCAL_DATE_TIME_WITH_NS_SPACE; + private static final DateTimeFormatter ISO_LOCAL_DATE_TIME_WITH_SLASH_NS_SPACE; + private static final DateTimeFormatter ISO_LOCAL_DATE_TIME_WITH_DOT_NS_SPACE; + + static { + ISO_LOCAL_DATE_TIME_WITH_NS_T = + new DateTimeFormatterBuilder() + .append(ISO_LOCAL_DATE_WIDTH_1_2) + .appendLiteral('T') + .append(ISO_LOCAL_TIME_WITH_NS) + .toFormatter(); + + ISO_LOCAL_DATE_TIME_WITH_SLASH_NS_T = + new DateTimeFormatterBuilder() + .append(ISO_LOCAL_DATE_WITH_SLASH) + .appendLiteral('T') + .append(ISO_LOCAL_TIME_WITH_NS) + .toFormatter(); + + ISO_LOCAL_DATE_TIME_WITH_DOT_NS_T = + new DateTimeFormatterBuilder() + .append(ISO_LOCAL_DATE_WITH_DOT) + .appendLiteral('T') + .append(ISO_LOCAL_TIME_WITH_NS) + .toFormatter(); + + ISO_LOCAL_DATE_TIME_WITH_NS_SPACE = + new DateTimeFormatterBuilder() + .append(ISO_LOCAL_DATE_WIDTH_1_2) + .appendLiteral(' ') + .append(ISO_LOCAL_TIME_WITH_NS) + .toFormatter(); + + ISO_LOCAL_DATE_TIME_WITH_SLASH_NS_SPACE = + new DateTimeFormatterBuilder() + .append(ISO_LOCAL_DATE_WITH_SLASH) + .appendLiteral(' ') + .append(ISO_LOCAL_TIME_WITH_NS) + .toFormatter(); + + ISO_LOCAL_DATE_TIME_WITH_DOT_NS_SPACE = + new DateTimeFormatterBuilder() + .append(ISO_LOCAL_DATE_WITH_DOT) + .appendLiteral(' ') + .append(ISO_LOCAL_TIME_WITH_NS) + .toFormatter(); + } + public static final DateTimeFormatter formatter = new DateTimeFormatterBuilder() /** @@ -537,6 +590,12 @@ public static void initTimestampPrecision() { /** such as '2011.12.03 10:15:30+01:00' or '2011.12.03 10:15:30.123456789+01:00'. */ .appendOptional(ISO_OFFSET_DATE_TIME_WITH_DOT_WITH_SPACE_NS) + .appendOptional(ISO_LOCAL_DATE_TIME_WITH_NS_T) + .appendOptional(ISO_LOCAL_DATE_TIME_WITH_SLASH_NS_T) + .appendOptional(ISO_LOCAL_DATE_TIME_WITH_DOT_NS_T) + .appendOptional(ISO_LOCAL_DATE_TIME_WITH_NS_SPACE) + .appendOptional(ISO_LOCAL_DATE_TIME_WITH_SLASH_NS_SPACE) + .appendOptional(ISO_LOCAL_DATE_TIME_WITH_DOT_NS_SPACE) .toFormatter() .withResolverStyle(ResolverStyle.STRICT); @@ -548,22 +607,54 @@ public static long convertTimestampOrDatetimeStrToLongWithDefaultZone(String tim } } + /** + * @deprecated Use {@link #convertDatetimeStrToLong(String, ZoneId, String)} instead. + */ + @Deprecated + public static long convertDatetimeStrToLong( + String str, ZoneOffset offset, String timestampPrecision) { + return convertDatetimeStrToLong(str, (ZoneId) offset, 0, timestampPrecision); + } + + /** + * @deprecated Use {@link #convertDatetimeStrToLong(String, ZoneId, int, String)} instead. + */ + @Deprecated + public static long convertDatetimeStrToLong( + String str, ZoneOffset offset, int depth, String timestampPrecision) { + return convertDatetimeStrToLong(str, (ZoneId) offset, depth, timestampPrecision); + } + public static long convertDatetimeStrToLong(String str, ZoneId zoneId) { return convertDatetimeStrToLong( - str, - toZoneOffset(zoneId), - 0, - CommonDescriptor.getInstance().getConfig().getTimestampPrecision()); + str, zoneId, 0, CommonDescriptor.getInstance().getConfig().getTimestampPrecision()); } public static long convertDatetimeStrToLong( String str, ZoneId zoneId, String timestampPrecision) { - return convertDatetimeStrToLong(str, toZoneOffset(zoneId), 0, timestampPrecision); + return convertDatetimeStrToLong(str, zoneId, 0, timestampPrecision); } + /** + * @deprecated Use {@link #getInstantWithPrecision(String, String, ZoneId)} instead. + */ + @Deprecated public static long getInstantWithPrecision(String str, String timestampPrecision) { - ZonedDateTime zonedDateTime = ZonedDateTime.parse(str, formatter); - Instant instant = zonedDateTime.toInstant(); + return getInstantWithPrecision(str, timestampPrecision, ZoneId.systemDefault()); + } + + public static long getInstantWithPrecision(String str, String timestampPrecision, ZoneId zoneId) { + TemporalAccessor parsed = formatter.parseBest(str, OffsetDateTime::from, LocalDateTime::from); + Instant instant; + if (parsed instanceof OffsetDateTime) { + instant = ((OffsetDateTime) parsed).toInstant(); + } else { + instant = ((LocalDateTime) parsed).atZone(zoneId).toInstant(); + } + return getInstantWithPrecision(instant, timestampPrecision); + } + + public static long getInstantWithPrecision(Instant instant, String timestampPrecision) { if ("us".equals(timestampPrecision) || "microsecond".equals(timestampPrecision)) { if (instant.getEpochSecond() < 0 && instant.getNano() > 0) { // adjustment can reduce the loss of the division @@ -583,22 +674,19 @@ public static long getInstantWithPrecision(String str, String timestampPrecision /** convert date time string to millisecond, microsecond or nanosecond. */ public static long convertDatetimeStrToLong( - String str, ZoneOffset offset, int depth, String timestampPrecision) { + String str, ZoneId zoneId, int depth, String timestampPrecision) { if (depth >= 2) { throw new DateTimeException( String.format( - "Failed to convert %s to millisecond, zone offset is %s, " + "Failed to convert %s to millisecond, zoneId is %s, " + "please input like 2011-12-03T10:15:30 or 2011-12-03T10:15:30+01:00", - str, offset)); + str, zoneId)); } if (str.contains("Z")) { return convertDatetimeStrToLong( - str.substring(0, str.indexOf('Z')) + "+00:00", offset, depth, timestampPrecision); + str.substring(0, str.indexOf('Z')) + "+00:00", zoneId, depth, timestampPrecision); } else if (str.length() == 10) { - return convertDatetimeStrToLong(str + "T00:00:00", offset, depth, timestampPrecision); - } else if (str.length() - str.lastIndexOf('+') != 6 - && str.length() - str.lastIndexOf('-') != 6) { - return convertDatetimeStrToLong(str + offset, offset, depth + 1, timestampPrecision); + return convertDatetimeStrToLong(str + "T00:00:00", zoneId, depth, timestampPrecision); } else if (str.contains("[") || str.contains("]")) { throw new DateTimeException( String.format( @@ -606,7 +694,7 @@ public static long convertDatetimeStrToLong( + "please input like 2011-12-03T10:15:30 or 2011-12-03T10:15:30+01:00", str)); } - return getInstantWithPrecision(str, timestampPrecision); + return getInstantWithPrecision(str, timestampPrecision, zoneId); } public static TimeUnit timestampPrecisionStringToTimeUnit(String timestampPrecision) { @@ -662,6 +750,7 @@ public static ZonedDateTime convertToZonedDateTime(long timestamp, ZoneId zoneId return ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), zoneId); } + @Deprecated() public static ZoneOffset toZoneOffset(ZoneId zoneId) { return zoneId.getRules().getOffset(Instant.now()); }