Skip to content

Commit

Permalink
Support years
Browse files Browse the repository at this point in the history
  • Loading branch information
Gilthoniel committed Dec 16, 2023
1 parent 24f00e3 commit ea391b7
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 14 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ This package implements the specification found in [Wikipedia](https://en.wikipe
| Day of month | 1-31 | , - * ? / L |
| Month | 1-12 | , - * / |
| Day of week | 0-6 or SUN-SAT | , - * ? / L # |
| Years | 1-9999 | , - * / |
8 changes: 2 additions & 6 deletions cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import (
"time"
)

const (
maxYearAttempts = 100
)

var defaultParser = cronParser{}

// TimeUnit represents a single part of a Cron expression.
Expand Down Expand Up @@ -51,7 +47,7 @@ func (s Schedule) Next(after time.Time) (next time.Time) {
var ok bool

for !ok {
if next.Year()-after.Year() > maxYearAttempts {
if next.IsZero() || next.Year() > rangeMaxYear {
// Return a zero time when the expression is unable to find a proper
// time after a given number of years.
return time.Time{}
Expand Down Expand Up @@ -84,7 +80,7 @@ func (s Schedule) Previous(before time.Time) (prev time.Time) {
var ok bool

for !ok {
if before.Year()-prev.Year() > maxYearAttempts {
if prev.IsZero() || prev.Year() > rangeMaxYear {
return time.Time{}
}
prev, ok = s.prevBefore(prev)
Expand Down
40 changes: 36 additions & 4 deletions cron_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ func TestSchedule_Upcoming_returnsNextNthWeekdayOfMonth(t *testing.T) {
testIterator(t, iter, expects)
}

func TestSchedule_Upcoming_returnsNextYear(t *testing.T) {
iter := MustParse("0 0 0 1 6 ? 2010-2012").Upcoming(time.Date(2000, time.March, 15, 12, 5, 1, 0, time.UTC))
expects := []string{
"2010-06-01 00:00:00 +0000 UTC",
"2011-06-01 00:00:00 +0000 UTC",
"2012-06-01 00:00:00 +0000 UTC",
}

testIterator(t, iter, expects)
}

func TestSchedule_Next_abortsExpressionWhichIsImpossible(t *testing.T) {
schedule := MustParse("* * * 31 2 ?")

Expand Down Expand Up @@ -149,11 +160,11 @@ func TestSchedule_Preceding_returnsPreviousHours(t *testing.T) {
}

func TestSchedule_Preceding_returnsPreviousDays(t *testing.T) {
iter := MustParse("0 0 0 28,31 * *").Preceding(time.Date(2000, time.March, 15, 12, 30, 0, 0, time.UTC))
iter := MustParse("30 30 12 28,31 * *").Preceding(time.Date(2000, time.March, 15, 12, 30, 0, 0, time.UTC))
expects := []string{
"2000-02-28 00:00:00 +0000 UTC",
"2000-01-31 00:00:00 +0000 UTC",
"2000-01-28 00:00:00 +0000 UTC",
"2000-02-28 12:30:30 +0000 UTC",
"2000-01-31 12:30:30 +0000 UTC",
"2000-01-28 12:30:30 +0000 UTC",
}

testIterator(t, iter, expects)
Expand All @@ -179,6 +190,27 @@ func TestSchedule_Preceding_returnsPreviousWeekDays(t *testing.T) {
testIterator(t, iter, expects)
}

func TestSchedule_Preceding_returnsPreviousYear(t *testing.T) {
iter := MustParse("0 0 0 1 6 ? 1900/5").Preceding(time.Date(2003, time.March, 15, 12, 5, 1, 0, time.UTC))
expects := []string{
"2000-06-01 00:00:00 +0000 UTC",
"1995-06-01 00:00:00 +0000 UTC",
"1990-06-01 00:00:00 +0000 UTC",
}

testIterator(t, iter, expects)
}

func TestSchedule_Preceding_returnsPreviousYearInList(t *testing.T) {
iter := MustParse("30 30 12 1 6 ? 1901,1902").Preceding(time.Date(2003, time.March, 15, 12, 5, 1, 0, time.UTC))
expects := []string{
"1902-06-01 12:30:30 +0000 UTC",
"1901-06-01 12:30:30 +0000 UTC",
}

testIterator(t, iter, expects)
}

func TestSchedule_Preceding_returnsPreviousInRange(t *testing.T) {
iter := MustParse("0-1 * * * * *").Preceding(time.Date(2000, time.March, 15, 12, 30, 0, 0, time.UTC))
expects := []string{
Expand Down
3 changes: 2 additions & 1 deletion error.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ const (
Days
Months
WeekDays
Years
)

var kinds = []string{"seconds", "minutes", "hours", "days", "months", "week days"}
var kinds = []string{"seconds", "minutes", "hours", "days", "months", "week days", "years"}

func (k TimeUnitKind) String() string {
return kinds[int(k)%len(kinds)]
Expand Down
14 changes: 14 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ func ExampleSchedule_Next_everyFifteenSeconds() {
// 2023-06-04 00:00:30 +0000 UTC
}

func ExampleSchedule_Next_usingTimezone() {
schedule := gocron.MustParse("*/15 * * * * *")

next := schedule.Next(time.Date(2023, time.June, 4, 0, 0, 0, 0, time.FixedZone("CEST", 120)))
fmt.Println(next)

next = schedule.Next(next)
fmt.Println(next)

// Output:
// 2023-06-04 00:00:15 +0002 CEST
// 2023-06-04 00:00:30 +0002 CEST
}

func ExampleSchedule_Next_everyLastFridayOfTheMonthAtMidnight() {
schedule := gocron.MustParse("0 0 0 ? * 5L")

Expand Down
13 changes: 12 additions & 1 deletion parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import (

const (
minExprMatches = 6
maxExprMatches = 6
maxExprMatches = 7
rangeSplitSize = 2
intervalSplitSize = 2
nthSplitSize = 2

rangeMinYear = 1
rangeMaxYear = 9999
rangeMinWeekday = 0
rangeMaxWeekday = 6
rangeMinMonth = 1
Expand Down Expand Up @@ -74,6 +76,15 @@ func (p cronParser) Parse(expression string) (schedule Schedule, err error) {
secTimeUnit(seconds),
}

if len(matches) == maxExprMatches {
years, err := p.parse(matches[maxExprMatches-1], convertUnit, rangeMinYear, rangeMaxYear)
if err != nil {
return schedule, newTimeUnitErr(Years, err)
}

schedule.timeUnits = append([]TimeUnit{yearTimeUnit(years)}, schedule.timeUnits...)
}

return schedule, err
}

Expand Down
8 changes: 8 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ func TestParser_Parse_abortsOnMalformedWeekDays(t *testing.T) {
requireSameKind(t, e.kind, WeekDays)
}

func TestParser_Parse_abortsOnMalformedYears(t *testing.T) {
_, err := defaultParser.Parse("* * * * * * a")

var e TimeUnitError
requireErrorAs(t, err, &e)
requireSameKind(t, e.kind, Years)
}

func TestParser_Parse_abortsOnTooBigLastValue(t *testing.T) {
_, err := defaultParser.Parse("* * * L-32 * *")
requireErrorIs(t, err, ErrValueOutsideRange)
Expand Down
25 changes: 23 additions & 2 deletions time-unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,26 @@ func (u weekdayTimeUnit) Previous(before time.Time) (time.Time, bool) {
return setDays(before, before.Day()-1), false
}

type yearTimeUnit []timeSet

func (u yearTimeUnit) Next(next time.Time) (time.Time, bool) {
candidate, ok := searchNextCandidate(u, next, time.Time.Year, setYears)
if ok {
return candidate, true
}

return setYears(next, rangeMaxYear+1), false
}

func (u yearTimeUnit) Previous(before time.Time) (time.Time, bool) {
candidate, ok := searchPrevCandidate(u, before, time.Time.Year, setYears)
if ok {
return candidate, true
}

return setYears(before, rangeMinYear), false
}

type getterFunc[T int | time.Month] func(time.Time) T
type setterFunc[T int | time.Month] func(time.Time, T) time.Time

Expand Down Expand Up @@ -225,8 +245,9 @@ func searchCandidate[T int | time.Month](
return setter(t, T(slices.Min(candidates))), true
}

// When iterating backwards, it uses the biggest candidate.
return setter(t, T(slices.Max(candidates))), true
// When iterating backwards, it uses the biggest candidate annd sets all
// the lower fields too their max value.
return setter(t, T(slices.Max(candidates))+1).Add(-time.Second), true
}

return time.Time{}, false
Expand Down

0 comments on commit ea391b7

Please sign in to comment.