Skip to content

Commit

Permalink
feat: add schedule.Prev() from robfig/pull/361
Browse files Browse the repository at this point in the history
  • Loading branch information
fufuok committed Feb 17, 2023
1 parent 0885992 commit 7b86320
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 0 deletions.
6 changes: 6 additions & 0 deletions constantdelay.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ func Every(duration time.Duration) ConstantDelaySchedule {
func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {
return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)
}

// Prev returns the previous time this should be run.
// It's not meaningful for ConstantDelaySchedule.
func (schedule ConstantDelaySchedule) Prev(t time.Time) time.Time {
return time.Time{}
}
3 changes: 3 additions & 0 deletions cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ type Schedule interface {
// Next returns the next activation time, later than the given time.
// Next is invoked initially, and then each time the job is run.
Next(time.Time) time.Time

// Prev returns the previous activation time, earlier than the given time.
Prev(time.Time) time.Time
}

// EntryID identifies an entry within a Cron instance
Expand Down
4 changes: 4 additions & 0 deletions cron_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,10 @@ func (*ZeroSchedule) Next(time.Time) time.Time {
return time.Time{}
}

func (*ZeroSchedule) Prev(time.Time) time.Time {
return time.Time{}
}

// Tests that job without time does not run
func TestJobWithZeroTimeDoesNotRun(t *testing.T) {
cron := newWithSeconds()
Expand Down
123 changes: 123 additions & 0 deletions spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,129 @@ WRAP:
return t.In(origLocation)
}

// Prev returns the previous time this schedule should be activated, less than the given
// time. If no time can be found to satisfy the schedule, return the zero time.
func (s *SpecSchedule) Prev(t time.Time) time.Time {
// General approach
//
// For Month, Day, Hour, Minute, Second:
// Check if the time value matches. If yes, continue to the next field.
// If the field doesn't match the schedule, then increment the field until it matches.
// While incrementing the field, a wrap-around brings it back to the beginning
// of the field list (since it is necessary to re-verify previous field
// values)

// Convert the given time into the schedule's timezone, if one is specified.
// Save the original timezone so we can convert back after we find a time.
// Note that schedules without a time zone specified (time.Local) are treated
// as local to the time provided.
origLocation := t.Location()
loc := s.Location
if loc == time.Local {
loc = t.Location()
}
if s.Location != time.Local {
t = t.In(s.Location)
}

// Start at the earliest possible time (the past second).
t = t.Add(-1*time.Second + time.Duration(t.Nanosecond())*time.Nanosecond)

// This flag indicates whether a field has been incremented.
added := false

// If no time is found within five years, return zero.
yearLimit := t.Year() - 5

WRAP:
if t.Year() < yearLimit {
return time.Time{}
}

// Find the first applicable month.
// If it's this month, then do nothing.
for 1<<uint(t.Month())&s.Month == 0 {
cur := t.Month()
// If we have to sub a month, reset the other parts to 0.
if !added {
added = true
}
// Otherwise, set the date at the beginning (since the current time is irrelevant).
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc)
t = t.Add(-1 * time.Second)

// Wrapped around.
if t.Month() > cur {
goto WRAP
}
}

// Now get a day in that month.
//
// NOTE: This causes issues for daylight savings regimes where midnight does
// not exist. For example: Sao Paulo has DST that transforms midnight on
// 11/3 into 1am. Handle that by noticing when the Hour ends up != 0.
for !dayMatches(s, t) {
cur := t.Day()
if !added {
added = true
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
t = t.Add(-1 * time.Second)
} else {
t = t.AddDate(0, 0, -1)
}

// Wrapped around.
if t.Day() > cur {
goto WRAP
}
}

for 1<<uint(t.Hour())&s.Hour == 0 {
cur := t.Hour()
if !added {
added = true
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc)
t = t.Add(-1 * time.Second)
} else {
t = t.Add(-1 * time.Hour)
}
if t.Hour() > cur {
goto WRAP
}
}

for 1<<uint(t.Minute())&s.Minute == 0 {
cur := t.Minute()
if !added {
added = true
t = t.Truncate(time.Minute)
t = t.Add(-1 * time.Second)
} else {
t = t.Add(-1 * time.Minute)
}

if t.Minute() > cur {
goto WRAP
}
}

for 1<<uint(t.Second())&s.Second == 0 {
cur := t.Second()
if !added {
added = true
t = t.Truncate(time.Second)
}
t = t.Add(-1 * time.Second)

if t.Second() > cur {
goto WRAP
}
}

return t.In(origLocation)
}

// dayMatches returns true if the schedule's day-of-week and day-of-month
// restrictions are satisfied by the given time.
func dayMatches(s *SpecSchedule, t time.Time) bool {
Expand Down
128 changes: 128 additions & 0 deletions spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,134 @@ func TestNext(t *testing.T) {
}
}

func TestPrev(t *testing.T) {
runs := []struct {
time, spec string
expected string
}{
// Simple cases
{"Mon Jul 9 14:45 2012", "0 0/15 * * * *", "Mon Jul 9 14:30 2012"},
{"Mon Jul 9 14:01 2012", "0 0/15 * * * *", "Mon Jul 9 14:00 2012"},
{"Mon Jul 9 14:00:01 2012", "0 0/15 * * * *", "Mon Jul 9 14:00 2012"},

// Wrap around hours
{"Mon Jul 9 15:20 2012", "0 20-35/15 * * * *", "Mon Jul 9 14:35 2012"},

// Wrap around days
{"Mon Jul 9 00:00 2012", "0 */15 * * * *", "Sun Jul 8 23:45 2012"},
{"Mon Jul 9 00:00 2012", "0 20-35/15 * * * *", "Sun Jul 8 23:35 2012"},
{"Mon Jul 9 00:15:51 2012", "15/35 20-35/15 * * * *", "Sun Jul 8 23:35:50 2012"},
{"Mon Jul 9 00:15:51 2012", "15/35 20-35/15 0/2 * * *", "Sun Jul 8 22:35:50 2012"},
{"Mon Jul 9 00:15:51 2012", "15/35 20-35/15 10-12 * * *", "Sun Jul 8 12:35:50 2012"},

{"Mon Jul 9 00:15:51 2012", "15/35 20-35/15 0/2 */2 * *", "Sat Jul 7 22:35:50 2012"},
{"Mon Jul 9 00:15:51 2012", "15/35 20-35/15 * 8-20 * *", "Sun Jul 8 23:35:50 2012"},
{"Mon Jul 9 00:15:51 2012", "15/35 20-35/15 * 8-20 Jul *", "Sun Jul 8 23:35:50 2012"},

// Wrap around months
{"Mon Jul 9 00:15 2012", "0 0 0 10 Apr-Oct ?", "Thu Jun 10 00:00 2012"},
{"Mon Jul 9 00:15 2012", "0 0 0 */5 Apr,Jun Mon", "Tue Jun 26 00:00 2012"},
{"Mon Jul 9 00:15 2012", "0 0 0 */5 Apr Mon", "Mon Apr 30 00:00 2012"},

// Wrap around years
{"Mon Jul 9 00:15 2012", "0 0 0 * Aug Mon", "Mon Aug 29 00:00 2011"},
{"Mon Jul 9 00:15 2012", "0 0 0 * Aug Mon/2", "Wed Aug 31 00:00 2011"},

// Wrap around minute, hour, day, month, and year
{"Sun Jan 1 00:00:00 2012", "0 * * * * *", "Sat Dec 31 23:59:00 2011"},

// Leap year
{"Mon Jul 9 00:15 2011", "0 0 0 29 Feb ?", "Fri Feb 29 00:00 2008"},

// Daylight savings time 2am EST (-5) -> 3am EDT (-4)
{"2012-03-11T04:00:00-0400", "TZ=America/New_York 0 30 2 11 Mar ?", "2011-03-11T02:30:00-0500"},

// hourly job
{"2012-03-11T01:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T00:00:00-0500"},
{"2012-03-11T03:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T01:00:00-0500"},
{"2012-03-11T04:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T03:00:00-0400"},
{"2012-03-11T05:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T04:00:00-0400"},

// hourly job using CRON_TZ
{"2012-03-11T01:00:00-0500", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T00:00:00-0500"},
{"2012-03-11T03:00:00-0400", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T01:00:00-0500"},
{"2012-03-11T04:00:00-0400", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T03:00:00-0400"},
{"2012-03-11T05:00:00-0400", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T04:00:00-0400"},

// 1am nightly job
{"2012-03-11T04:00:00-0400", "TZ=America/New_York 0 0 1 * * ?", "2012-03-11T01:00:00-0500"},
{"2012-03-12T04:00:00-0400", "TZ=America/New_York 0 0 1 * * ?", "2012-03-12T01:00:00-0400"},

// 2am nightly job (skipped)
{"2012-03-11T04:00:00-0400", "TZ=America/New_York 0 0 2 * * ?", "2012-03-10T02:00:00-0500"},

// Daylight savings time 2am EDT (-4) => 1am EST (-5)
{"2012-11-04T01:45:00-0500", "TZ=America/New_York 0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"},
{"2012-11-04T01:45:00-0400", "TZ=America/New_York 0 30 1 04 Nov ?", "2012-11-04T01:30:00-0400"},

// hourly job
{"2012-11-04T01:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T00:00:00-0400"},
{"2012-11-04T01:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T01:00:00-0400"},
{"2012-11-04T02:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T01:00:00-0500"},

// 1am nightly job (runs twice)
{"2012-11-04T01:00:00-0400", "TZ=America/New_York 0 0 1 * * ?", "2012-11-03T01:00:00-0400"},
{"2012-11-04T01:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-11-04T01:00:00-0400"},
{"2012-11-04T05:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-11-04T01:00:00-0500"},

// 2am nightly job
{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 2 * * ?", "2012-11-03T02:00:00-0400"},
{"2012-11-04T05:00:00-0500", "TZ=America/New_York 0 0 2 * * ?", "2012-11-04T02:00:00-0500"},

// 3am nightly job
{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 3 * * ?", "2012-11-03T03:00:00-0400"},
{"2012-11-04T05:00:00-0500", "TZ=America/New_York 0 0 3 * * ?", "2012-11-04T03:00:00-0500"},

// hourly job
{"TZ=America/New_York 2012-11-04T01:00:00-0400", "0 0 * * * ?", "2012-11-04T00:00:00-0400"},
{"TZ=America/New_York 2012-11-04T01:00:00-0500", "0 0 * * * ?", "2012-11-04T01:00:00-0400"},
{"TZ=America/New_York 2012-11-04T02:00:00-0500", "0 0 * * * ?", "2012-11-04T01:00:00-0500"},

// 1am nightly job (runs twice)
{"TZ=America/New_York 2012-11-04T01:00:00-0400", "0 0 1 * * ?", "2012-11-03T01:00:00-0400"},
{"TZ=America/New_York 2012-11-04T01:00:00-0500", "0 0 1 * * ?", "2012-11-04T01:00:00-0400"},
{"TZ=America/New_York 2012-11-04T05:00:00-0500", "0 0 1 * * ?", "2012-11-04T01:00:00-0500"},

// 2am nightly job
{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 2 * * ?", "2012-11-03T02:00:00-0400"},
{"TZ=America/New_York 2012-11-04T05:00:00-0500", "0 0 2 * * ?", "2012-11-04T02:00:00-0500"},

// 3am nightly job
{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 3 * * ?", "2012-11-03T03:00:00-0400"},
{"TZ=America/New_York 2012-11-04T05:00:00-0500", "0 0 3 * * ?", "2012-11-04T03:00:00-0500"},

// Unsatisfiable
{"Mon Jul 9 00:15 2012", "0 0 0 30 Feb ?", ""},
{"Mon Jul 9 00:15 2012", "0 0 0 31 Apr ?", ""},

// Monthly job
{"TZ=America/New_York 2012-12-01T00:00:00-0500", "0 0 3 3 * ?", "2012-11-03T03:00:00-0400"},

// Test the scenario of DST resulting in midnight not being a valid time.
// https://github.com/robfig/cron/issues/157
{"2018-12-07T05:00:00-0500", "TZ=America/Sao_Paulo 0 0 9 10 * ?", "2018-11-10T06:00:00-0500"},
{"2018-03-14T05:00:00-0400", "TZ=America/Sao_Paulo 0 0 9 22 * ?", "2018-02-22T07:00:00-0500"},
}

for _, c := range runs {
sched, err := secondParser.Parse(c.spec)
if err != nil {
t.Error(err)
continue
}
actual := sched.Prev(getTime(c.time))
expected := getTime(c.expected)
if !actual.Equal(expected) {
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual)
}
}
}

func TestErrors(t *testing.T) {
invalidSpecs := []string{
"xyz",
Expand Down

0 comments on commit 7b86320

Please sign in to comment.