Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion internal/scheduler/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,17 @@ func (s *Schedule) matches(t time.Time) bool {
s.minutes.matches(t.Minute()) &&
s.hours.matches(t.Hour()) &&
s.months.matches(int(t.Month())) &&
(s.days.matches(t.Day()) || s.weekdays.matches(int(t.Weekday())))
(s.days.matches(t.Day()) || s.matchesWeekday(t.Weekday()))
}

// matchesWeekday reports whether the weekday field matches wd. Cron accepts both
// 0 and 7 as Sunday, but time.Weekday only ranges 0 (Sunday) to 6 (Saturday), so
// a Sunday must also be checked against 7 to honor expressions written that way.
func (s *Schedule) matchesWeekday(wd time.Weekday) bool {
if s.weekdays.matches(int(wd)) {
return true
}
return wd == time.Sunday && s.weekdays.matches(7)
}

// parseField parses a single cron field (supports *, ranges, lists, steps).
Expand Down
38 changes: 38 additions & 0 deletions internal/scheduler/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,41 @@ func TestScheduleNextWithTimezone(t *testing.T) {
t.Errorf("Next(%v) = %v, expected %v", from, next, expected)
}
}

func TestScheduleWeekdaySevenIsSunday(t *testing.T) {
// Cron treats both 0 and 7 as Sunday, but time.Weekday never returns 7,
// so a schedule written with 7 must still fire on Sundays.
//
// Combine day-of-month 1 with weekday 7 so the day-of-month clause does not
// mask the weekday behavior for the (non-first-of-month) days exercised here.
// 2025-01-05 is a Sunday and 2025-01-06 is a Monday.
sunday := time.Date(2025, 1, 5, 0, 0, 0, 0, time.UTC)
monday := time.Date(2025, 1, 6, 0, 0, 0, 0, time.UTC)

seven, err := ParseCronExpression("0 0 1 * 7")
if err != nil {
t.Fatalf("failed to parse expression: %v", err)
}
if !seven.matches(sunday) {
t.Errorf("weekday 7 should match Sunday %v", sunday)
}
if seven.matches(monday) {
t.Errorf("weekday 7 should not match Monday %v", monday)
}

// Regression: weekday 0 (the canonical Sunday) keeps working.
zero, err := ParseCronExpression("0 0 1 * 0")
if err != nil {
t.Fatalf("failed to parse expression: %v", err)
}
if !zero.matches(sunday) {
t.Errorf("weekday 0 should match Sunday %v", sunday)
}

// End-to-end via the public Next: from a Thursday, the next fire of
// "0 0 1 * 7" is the upcoming Sunday (Jan 5), earlier than the next 1st.
from := time.Date(2025, 1, 2, 10, 0, 0, 0, time.UTC)
if next := seven.Next(from); !next.Equal(sunday) {
t.Errorf("Next(%v) = %v, expected Sunday %v", from, next, sunday)
}
}