Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines 558 to +568
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convertDatetimeStrToLong(String, ZoneId) now calls toZoneOffset(str, zoneId), and toZoneOffset reparses the same string via convertDatetimeStrToLong(str, ZoneOffset.UTC, ...) before convertDatetimeStrToLong parses it again with the resolved offset. This introduces an extra full parse for datetime strings without an explicit offset, which could be a hot-path performance regression. If this method is performance-sensitive, consider extracting the local date-time once (or otherwise avoiding the double parse) while still supporting the various input formats.

Copilot uses AI. Check for mistakes.
}

public static long getInstantWithPrecision(String str, String timestampPrecision) {
Expand Down Expand Up @@ -821,8 +821,22 @@ 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());
/** Converts string to ZoneOffset. Truncates seconds for HH:mm database compatibility. */
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new toZoneOffset(String, ZoneId) Javadoc says it “Truncates seconds for HH:mm database compatibility”, but the implementation actually truncates offset seconds (i.e., converts the computed ZoneOffset to minute granularity). Consider clarifying the comment to avoid implying that input datetime seconds are truncated, and document how DST gaps/overlaps are resolved (since this method now derives offsets from local date-times).

Suggested change
/** Converts string to ZoneOffset. Truncates seconds for HH:mm database compatibility. */
/**
* Converts a date-time string to a {@link ZoneOffset}.
*
* <p>If the input ends with {@code Z}, this method returns {@link ZoneOffset#UTC}. If the
* input contains an explicit {@code \u00b1HH:mm} offset, that offset is returned directly.
* Otherwise, the input is interpreted as a local date-time in the supplied {@code zoneId}, and
* the corresponding zone offset is derived from that local date-time.
*
* <p>For database {@code HH:mm} compatibility, this method truncates the resulting offset to
* minute precision by discarding any offset seconds. It does not truncate the seconds field of
* the input date-time.
*
* <p>When deriving the offset from a local date-time, DST transitions are resolved using {@link
* java.time.zone.ZoneRules#getOffset(LocalDateTime)}: for overlaps, the earlier offset is
* returned; for gaps, the offset before the transition is returned.
*/

Copilot uses AI. Check for mistakes.
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);
ZoneOffset offset = zoneId.getRules().getOffset(localDateTime);
return ZoneOffset.ofTotalSeconds((offset.getTotalSeconds() / 60) * 60);
}

public static ZonedDateTime convertMillsecondToZonedDateTime(long millisecond) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -338,4 +340,95 @@ 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);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testToZoneOffsetJustBeforeSpringDST uses 2024-03-31 02:00:00 for Europe/Warsaw, but that local time is inside the spring-forward DST gap (the clock jumps from 02:00 to 03:00), so the test name/intent doesn’t match the chosen timestamp and the expected offset is ambiguous. Consider changing this to a valid instant just before the transition (e.g., 01:59:59) to make the assertion deterministic and aligned with the test name.

Suggested change
ZoneOffset offset = DateTimeUtils.toZoneOffset("2024-03-31 02:00:00", zoneId);
ZoneOffset offset = DateTimeUtils.toZoneOffset("2024-03-31 01:59:59", zoneId);

Copilot uses AI. Check for mistakes.
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);
}
Comment on lines +372 to +377
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testToZoneOffsetDuringSpringDSTGap asserts an offset for 2024-03-31 02:30:00 in Europe/Warsaw, but this local time does not exist due to the DST spring-forward gap. The current expectation relies on how gaps are resolved by ZoneRules and can become brittle across JDK/tzdb changes. It would be more robust to either (a) use ZoneRules#getValidOffsets/transition logic in production code and assert that documented behavior here, or (b) change the test to expect a failure/explicit resolution rather than asserting a specific offset for a nonexistent local time.

Copilot uses AI. Check for mistakes.

@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);
});
}

@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));
}
}