jmrozanec / jmrozanec/cron-utils

Bug: ExecutionTime incorrectly skips month intervals e.g. "0 0 0 15 10/3 ? *"

Open
#692 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
1.2k
Forks
272
PR merge metrics
No merged PRs in 30d

Description

I'm using this in my Android Kotlin app and I noticed this bug.

Based on the javadoc comment describing the [Every](https://github.com/jmrozanec/cron-utils/blob/6b7b06abf21ea9c8196672770731c42828f96cd9/src/main/java/com/cronutils/model/field/expression/Every.java#L24)field expression, I expected this to work. I tried with a custom built cron using Every as described, but I got the same result.

Library version used: 9.2.1

Expectation: should return next execution date correctly. Should return execution dates for date range correctly.

Actual: only returns execution dates for the starting month each year.

```
import com.cronutils.model.CronType
import com.cronutils.model.definition.CronDefinitionBuilder
import com.cronutils.model.time.ExecutionTime
import com.cronutils.parser.CronParser
import org.junit.Assert.assertEquals
import org.junit.Test
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZonedDateTime
import kotlin.jvm.optionals.getOrNull

class MonthlyCronTest {

private val cronParser = CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ))

private fun LocalDate.toZoned(): ZonedDateTime =
ZonedDateTime.of(this, LocalTime.MIDNIGHT, ZoneId.systemDefault())

@Test
fun next_execution_for_month_interval() {
// Cron expression for "every 3 months starting in October"
val cron = cronParser.parse("0 0 0 * 10/3 ? *")
val executionTime = ExecutionTime.forCron(cron)

// Reference date: February 1, 2026
val referenceDate = LocalDate.of(2026, 2, 1).toZoned()

// Expected result: April 1, 2026
val expected = LocalDate.of(2026, 4, 1)

// Actual result: October 1, 2026
val actual = executionTime.nextExecution(referenceDate).getOrNull()?.toLocalDate()

// This assertion fails
assertEquals(expected, actual)
}

@Test
fun next_execution_for_month_interval_with_specific_day_of_month() {
// Cron expression for "15th of the month, every 3 months starting in October"
val cron = cronParser.parse("0 0 0 15 10/3 ? *")
val executionTime = ExecutionTime.forCron(cron)

// Reference date: January 1, 2026
val referenceDate = LocalDate.of(2026, 1, 1).toZoned()

// Expected result: January 15, 2026
val expected = LocalDate.of(2026, 1, 15)

// Actual result: October 15, 2026
val actual = executionTime.nextExecution(referenceDate).getOrNull()?.toLocalDate()

// This assertion fails
assertEquals(expected, actual)
}

@Test
fun execution_dates_for_month_interval() {
// Cron expression for "15th of the month, every 3 months starting in October"
val cron = cronParser.parse("0 0 0 15 10/3 ? *")
val executionTime = ExecutionTime.forCron(cron)

// Date range of Oct 1, 2025 to Oct 31, 2026
val startDate = LocalDate.of(2025, 10, 1).toZoned()
val endDate = LocalDate.of(2026, 10, 31).toZoned()

// Expected result:
// - Oct 15, 2025
// - Jan 15, 2026
// - Apr 15, 2026
// - Jul 15, 2026
// - Oct 15, 2026
val expected = listOf(
LocalDate.of(2025, 10, 15).toZoned(),
LocalDate.of(2026, 1, 15).toZoned(),
LocalDate.of(2026, 4, 15).toZoned(),
LocalDate.of(2026, 7, 15).toZoned(),
LocalDate.of(2026, 10, 15).toZoned()
)

// Actual result:
// - Oct 15, 2025
// - Oct 15, 2026
val actual = executionTime.getExecutionDates(startDate, endDate)

// This assertion fails
assertEquals(expected, actual)
}
}
```

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.