Fix GetPreviousOccurrence returning a day in a disallowed month

When the time-of-day search rolls back past the 1st of the month, dayLimit
(and maxDay) can drop below the first valid day of month. The day-of-month
'*' fast path in TryGetPreviousDay returned actualDay = 0, and
DateTimeToTicks(year, month, 0, ...) underflowed into the last day of the
previous month, which is not re-validated against the month field. Guard
against maxDay below the first day of month so the caller advances to the
previous allowed month instead.
This commit is contained in:
dualfroz committed 2026-09-04 09:32:17 +02:00
1 parent 9daaf8b304
commit e055ee7ac6
3 files changed
+160

No files matched your search

@@ -125,6 +125,30 @@ namespace Cronos.Tests
Assert.Equal(GetUtcDateTime(expectedString), previous);
}
[Theory]
// 'from' is the 1st of an allowed month, but earlier than the first matching
// time-of-day. The search rolls the time back past midnight of the 1st, so it
// must skip to the previous allowed month instead of returning a day in the
// preceding (not allowed) month.
[InlineData("0 17 * 7 *", "2017-07-01 14:00:00", "2016-07-31 17:00:00")]
[InlineData("37-57/3 17-21 * 7 *", "2017-07-01 14:20:00", "2016-07-31 21:55:00")]
[InlineData("0 0 * 3 *", "2020-03-01 00:00:00", "2019-03-31 00:00:00")]
// Control: when the month field is unrestricted, rolling back into the previous
// month is correct and must not be skipped.
[InlineData("0 17 * * *", "2017-07-01 14:00:00", "2017-06-30 17:00:00")]
public void GetPreviousOccurrence_DoesNotReturnDayInDisallowedMonth_WhenTimeRollsBackPastFirstOfMonth(
string cronExpression, string fromString, string expectedString)
{
var expression = CronExpression.Parse(cronExpression, CronFormat.Standard);
var from = GetUtcDateTime(fromString);
var previous = expression.GetPreviousOccurrence(from, inclusive: false);
Assert.Equal(GetUtcDateTime(expectedString), previous);
}
[Fact]
public void GetPreviousOccurrence_ReturnsCorrectDate_WhenMacroExpressionHasJitterSeed()
{