From a870467b1bc285b97c24e9e11a7f59b891f00eb6 Mon Sep 17 00:00:00 2001 From: Lexert19 Date: Mon, 6 Apr 2026 18:10:03 +0200 Subject: [PATCH 1/2] [IOTDB-17433] Fix timezone offset bug in DateTimeUtils --- .../apache/iotdb/db/utils/DateTimeUtils.java | 20 ++++- .../iotdb/db/utils/DateTimeUtilsTest.java | 83 ++++++++++++++++++- 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/DateTimeUtils.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/DateTimeUtils.java index 63e577407cb02..bd8e18dbf8eea 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/DateTimeUtils.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/DateTimeUtils.java @@ -558,14 +558,14 @@ public static long convertTimestampOrDatetimeStrToLongWithDefaultZone(String tim public static long convertDatetimeStrToLong(String str, ZoneId zoneId) { return convertDatetimeStrToLong( str, - toZoneOffset(zoneId), + toZoneOffset(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, toZoneOffset(str, zoneId), 0, timestampPrecision); } public static long getInstantWithPrecision(String str, String timestampPrecision) { @@ -821,8 +821,20 @@ public static ZonedDateTime convertToZonedDateTime(long timestamp, ZoneId zoneId return ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), zoneId); } - public static ZoneOffset toZoneOffset(ZoneId zoneId) { - return zoneId.getRules().getOffset(Instant.now()); + public static ZoneOffset toZoneOffset(String str, ZoneId zoneId) { + if (str.endsWith("Z")) { + return ZoneOffset.UTC; + } + + int offsetIndex = Math.max(str.lastIndexOf('+'), str.lastIndexOf('-')); + if (offsetIndex != -1 && str.length() - offsetIndex == 6) { + return ZoneOffset.of(str.substring(offsetIndex)); + } + + long millis = convertDatetimeStrToLong(str, ZoneOffset.UTC, 0, "ms"); + LocalDateTime localDateTime = + LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneOffset.UTC); + return zoneId.getRules().getOffset(localDateTime); } public static ZonedDateTime convertMillsecondToZonedDateTime(long millisecond) { 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 d0bbb97fbae85..12bbdf3378cc5 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 @@ -27,12 +27,14 @@ import org.junit.Ignore; import org.junit.Test; +import java.time.DateTimeException; +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; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; public class DateTimeUtilsTest { @@ -47,7 +49,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; @@ -338,4 +340,81 @@ public void testConstructTimeDuration() { timeDuration = DateTimeUtils.constructTimeDuration("10000000000ms"); Assert.assertEquals(10000000000L, timeDuration.nonMonthDuration); } + + @Test + public void testToZoneOffsetForWinterTime() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + ZoneOffset offset = DateTimeUtils.toZoneOffset("2024-01-15 12:00:00", zoneId); + Assert.assertEquals(ZoneOffset.ofHours(1), offset); + } + + @Test + public void testToZoneOffsetForSummerTime() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + ZoneOffset offset = DateTimeUtils.toZoneOffset("2024-06-15 12:00:00", zoneId); + Assert.assertEquals(ZoneOffset.ofHours(2), offset); + } + + @Test + public void testToZoneOffsetJustBeforeSpringDST() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + ZoneOffset offset = DateTimeUtils.toZoneOffset("2024-03-31 02:00:00", zoneId); + Assert.assertEquals(ZoneOffset.ofHours(1), offset); + } + + @Test + public void testToZoneOffsetJustAfterSpringDST() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + ZoneOffset offset = DateTimeUtils.toZoneOffset("2024-03-31 03:00:00", zoneId); + Assert.assertEquals(ZoneOffset.ofHours(2), offset); + } + + @Test + public void testToZoneOffsetDuringSpringDSTGap() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + ZoneOffset offset = DateTimeUtils.toZoneOffset("2024-03-31 02:30:00", zoneId); + Assert.assertEquals(ZoneOffset.ofHours(1), offset); + } + + @Test + public void testToZoneOffsetDuringAutumnDSTTransition() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + ZoneOffset offset = DateTimeUtils.toZoneOffset("2024-10-27 02:30:00", zoneId); + Assert.assertEquals(ZoneOffset.ofHours(2), offset); + } + + @Test + public void testToZoneOffsetAfterAutumnDST() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + ZoneOffset offset = DateTimeUtils.toZoneOffset("2024-10-27 03:00:00", zoneId); + Assert.assertEquals(ZoneOffset.ofHours(1), offset); + } + + @Test + public void testToZoneOffsetWithExplicitOffsetInString() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + ZoneOffset offset = DateTimeUtils.toZoneOffset("2024-06-15 12:00:00+02:00", zoneId); + Assert.assertEquals(ZoneOffset.ofHours(2), offset); + offset = DateTimeUtils.toZoneOffset("2024-06-15 12:00:00Z", zoneId); + Assert.assertEquals(ZoneOffset.UTC, offset); + offset = DateTimeUtils.toZoneOffset("2024-06-15 12:00:00-08:00", zoneId); + Assert.assertEquals(ZoneOffset.ofHours(-8), offset); + } + + @Test + public void testToZoneOffsetWithUTCZoneId() { + ZoneId utc = ZoneId.of("UTC"); + Assert.assertEquals(ZoneOffset.UTC, DateTimeUtils.toZoneOffset("2024-06-15 12:00:00", utc)); + } + + @Test + public void testToZoneOffsetWithBrokenDate() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + DateTimeException exception = + assertThrows( + DateTimeException.class, + () -> { + DateTimeUtils.toZoneOffset("2024-12-31 10", zoneId); + }); + } } From 1ddb460585ea92285c919412e6566baf85c91227 Mon Sep 17 00:00:00 2001 From: Lexert19 Date: Thu, 9 Apr 2026 18:03:17 +0200 Subject: [PATCH 2/2] truncate seconds in toZoneOffset for DB compatibility --- .../org/apache/iotdb/db/utils/DateTimeUtils.java | 4 +++- .../apache/iotdb/db/utils/DateTimeUtilsTest.java | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/DateTimeUtils.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/DateTimeUtils.java index bd8e18dbf8eea..fdfafa38fbb67 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/DateTimeUtils.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/DateTimeUtils.java @@ -821,6 +821,7 @@ public static ZonedDateTime convertToZonedDateTime(long timestamp, ZoneId zoneId return ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), zoneId); } + /** Converts string to ZoneOffset. Truncates seconds for HH:mm database compatibility. */ public static ZoneOffset toZoneOffset(String str, ZoneId zoneId) { if (str.endsWith("Z")) { return ZoneOffset.UTC; @@ -834,7 +835,8 @@ public static ZoneOffset toZoneOffset(String str, ZoneId zoneId) { long millis = convertDatetimeStrToLong(str, ZoneOffset.UTC, 0, "ms"); LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneOffset.UTC); - return zoneId.getRules().getOffset(localDateTime); + ZoneOffset offset = zoneId.getRules().getOffset(localDateTime); + return ZoneOffset.ofTotalSeconds((offset.getTotalSeconds() / 60) * 60); } public static ZonedDateTime convertMillsecondToZonedDateTime(long millisecond) { 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 12bbdf3378cc5..9ec2f871b91be 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 @@ -417,4 +417,18 @@ public void testToZoneOffsetWithBrokenDate() { DateTimeUtils.toZoneOffset("2024-12-31 10", zoneId); }); } + + @Test + public void testToZoneOffsetHistoricalLMT() { + ZoneId zoneId = ZoneId.of("Europe/Warsaw"); + ZoneOffset offset = DateTimeUtils.toZoneOffset("0001-01-01 00:00:00", zoneId); + Assert.assertEquals(ZoneOffset.ofHoursMinutes(1, 24), offset); + + ZoneId shanghaiId = ZoneId.of("Asia/Shanghai"); + ZoneOffset shanghaiOffset = DateTimeUtils.toZoneOffset("0001-01-01 00:00:00", shanghaiId); + Assert.assertEquals(ZoneOffset.ofHoursMinutes(8, 5), shanghaiOffset); + + ZoneId utcId = ZoneId.of("UTC"); + Assert.assertEquals(ZoneOffset.UTC, DateTimeUtils.toZoneOffset("0001-01-01 00:00:00", utcId)); + } }