Skip to content

Commit 65913c3

Browse files
committed
Fix "Nth day of week" Quartz-style cron expressions
Prior to this commit, `CronExpression` would support Quartz-style expressions with "Nth occurence of a dayOfWeek" semantics by using the `TemporalAdjusters.dayOfWeekInMonth` JDK support. This method will return the Nth occurence starting with the month of the given temporal, but in some cases will overflow to the next or previous month. This behavior is not expected for our cron expression support. This commit ensures that when an overflow happens (meaning, the resulting date is not in the same month as the input temporal), we should instead have another attempt at finding a valid month for this expression. Fixes gh-34377
1 parent b7a996a commit 65913c3

File tree

2 files changed

+27
-4
lines changed

2 files changed

+27
-4
lines changed

spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -317,8 +317,16 @@ private static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek) {
317317
private static TemporalAdjuster dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek) {
318318
TemporalAdjuster adjuster = TemporalAdjusters.dayOfWeekInMonth(ordinal, dayOfWeek);
319319
return temporal -> {
320-
Temporal result = adjuster.adjustInto(temporal);
321-
return rollbackToMidnight(temporal, result);
320+
// TemporalAdjusters can overflow to a different month
321+
// in this case, attempt the same adjustment with the next/previous month
322+
for (int i = 0; i < 12; i++) {
323+
Temporal result = adjuster.adjustInto(temporal);
324+
if (result.get(ChronoField.MONTH_OF_YEAR) == temporal.get(ChronoField.MONTH_OF_YEAR)) {
325+
return rollbackToMidnight(temporal, result);
326+
}
327+
temporal = result;
328+
}
329+
return null;
322330
};
323331
}
324332

spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,6 +39,7 @@
3939
import static org.assertj.core.api.Assertions.assertThat;
4040

4141
/**
42+
* Tests for {@link CronExpression}.
4243
* @author Arjen Poutsma
4344
*/
4445
class CronExpressionTests {
@@ -1092,6 +1093,20 @@ void quartz2ndFridayOfTheMonthDayName() {
10921093
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
10931094
}
10941095

1096+
@Test
1097+
void quartz5thMondayOfTheMonthDayName() {
1098+
CronExpression expression = CronExpression.parse("0 0 0 ? * MON#5");
1099+
1100+
LocalDateTime last = LocalDateTime.of(2025, 1, 1, 0, 0, 0);
1101+
1102+
// first occurrence of 5 mondays in a month from last
1103+
LocalDateTime expected = LocalDateTime.of(2025, 3, 31, 0, 0, 0);
1104+
LocalDateTime actual = expression.next(last);
1105+
assertThat(actual).isNotNull();
1106+
assertThat(actual).isEqualTo(expected);
1107+
assertThat(actual.getDayOfWeek()).isEqualTo(MONDAY);
1108+
}
1109+
10951110
@Test
10961111
void quartzFifthWednesdayOfTheMonth() {
10971112
CronExpression expression = CronExpression.parse("0 0 0 ? * 3#5");

0 commit comments

Comments
 (0)