jmrozanec / jmrozanec/cron-utils

nW in day of month throws DateTimeException when the month is shorter than n

Open Beginner friendly
#715 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
1.2k
Forks
272
PR merge metrics
No merged PRs in 30d

Description

`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

```java
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:

```java
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`).

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in OnDayOfMonthValueGenerator.generateValue, focusing on case W and comparing it with the case L branch. Reproduce the Quartz example for 31W and inspect the relevant generator tests or add regression coverage. Done means shorter candidate months are skipped and the listed Quartz dates are produced for both QUARTZ and SPRING53 without a DateTimeException.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
tooling
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
84/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.