From cddc671a8c6eea118b1f7ef05f2b6d754e852f09 Mon Sep 17 00:00:00 2001 From: arno Date: Wed, 3 Jun 2026 15:16:16 +0800 Subject: [PATCH] Fix CronExpression day skip on midnight DST gap After rollForward, BitsCronField always searched for the next matching bit from zero. When daylight saving creates a gap at the start of a period (e.g. Africa/Cairo), the temporal lands on a non-zero field value and matching from zero could advance an entire period too far, skipping the calendar day. Search from the actual field value in the new period instead, falling back to zero only when no bit matches in that period. See gh-36865 Signed-off-by: arno Co-authored-by: Cursor --- .../scheduling/support/BitsCronField.java | 12 ++++++++++-- .../scheduling/support/CronExpressionTests.java | 8 ++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java index 59043762dc4..ef6d34347c5 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java @@ -177,7 +177,11 @@ final class BitsCronField extends CronField { int next = nextSetBit(current); if (next == -1) { temporal = type().rollForward(temporal); - next = nextSetBit(0); + next = nextSetBit(type().get(temporal)); + if (next == -1) { + temporal = type().rollForward(temporal); + next = nextSetBit(0); + } } if (next == current) { return temporal; @@ -191,7 +195,11 @@ final class BitsCronField extends CronField { next = nextSetBit(current); if (next == -1) { temporal = type().rollForward(temporal); - next = nextSetBit(0); + next = nextSetBit(type().get(temporal)); + if (next == -1) { + temporal = type().rollForward(temporal); + next = nextSetBit(0); + } } } if (count >= CronExpression.MAX_ATTEMPTS) { diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java index a60c64e2526..c35abbee654 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java @@ -1367,6 +1367,14 @@ class CronExpressionTests { actual = cronExpression.next(last); assertThat(actual).isNotNull(); assertThat(actual).isEqualTo(expected); + + cronExpression = CronExpression.parse("0 0 */2 * * ?"); + + last = ZonedDateTime.parse("2025-04-24T22:00:00+02:00[Africa/Cairo]"); + expected = ZonedDateTime.parse("2025-04-25T02:00:00+03:00[Africa/Cairo]"); + actual = cronExpression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); } @Test