nW (nearest weekday to the nth of the month) throws java.time.DateTimeException instead of skipping months that have fewer than n days. Affects QUARTZ and SPRING53 (any definition with supportsW() on day of month).
Reproduction
CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ));
ExecutionTime et = ExecutionTime.forCron(parser.parse("0 0 0 31W * ?"));
et.nextExecution(ZonedDateTime.parse("2025-06-01T00:00:00Z"));
Actual
java.time.DateTimeException: Invalid date 'JUNE 31'
Same for SPRING53. 30W evaluated from a 30-day month works fine, so the failure is specifically "requested day exceeds the length of the candidate month".
Expected
Quartz skips such months. Reference behaviour from org.quartz.CronExpression 2.5.0, same expression and start instant:
0 0 0 31W * ? -> 2025-07-31, 2025-08-29, 2025-10-31, 2025-12-31, 2026-01-30
June, September and November (30 days) and February are skipped; 2025-08-29 and 2026-01-30 show the weekday adjustment applied after the month is accepted.
Cause
OnDayOfMonthValueGenerator.generateValue, case W: constructs the date before checking the month length:
case W: // First work day of the week
final LocalDate doM = LocalDate.of(year, month, dayOfMonth);
LocalDate.of throws for dayOfMonth > lengthOfMonth. The case L: branch above it reads lengthOfMonth() first and is unaffected. A guard that reports no value for the month (so the generator moves to the next one) would line up with the reference behaviour.
Version: reproduced on master (bac6e86).
nW(nearest weekday to the nth of the month) throwsjava.time.DateTimeExceptioninstead of skipping months that have fewer thanndays. AffectsQUARTZandSPRING53(any definition withsupportsW()on day of month).Reproduction
Actual
Same for
SPRING53.30Wevaluated from a 30-day month works fine, so the failure is specifically "requested day exceeds the length of the candidate month".Expected
Quartz skips such months. Reference behaviour from
org.quartz.CronExpression2.5.0, same expression and start instant:June, September and November (30 days) and February are skipped;
2025-08-29and2026-01-30show the weekday adjustment applied after the month is accepted.Cause
OnDayOfMonthValueGenerator.generateValue,case W:constructs the date before checking the month length:LocalDate.ofthrows fordayOfMonth > lengthOfMonth. Thecase L:branch above it readslengthOfMonth()first and is unaffected. A guard that reports no value for the month (so the generator moves to the next one) would line up with the reference behaviour.Version: reproduced on
master(bac6e86).