Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add previous #216

Closed
wants to merge 2 commits into from
Closed
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
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 latest time this should be run.
// This rounds so that the latest activation time will be on the second.
func (schedule ConstantDelaySchedule) Prev(t time.Time) time.Time {
return t.Add(-schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)
}
3 changes: 3 additions & 0 deletions cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,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 next activation time, smaller or equal with the given time.
// Prev is invoked initially, and then each time the job is run.
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 @@ -541,6 +541,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
91 changes: 90 additions & 1 deletion spec.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cron

import "time"
import (
"time"
)

// SpecSchedule specifies a duty cycle (to the second granularity), based on a
// traditional crontab specification. It is computed initially and stored as bit sets.
Expand Down Expand Up @@ -174,6 +176,93 @@ WRAP:
return t.In(origLocation)
}

// Prev returns the lastest time this schedule is activated, smaller or equal with 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 upcoming second).
t = t.Add(-time.Duration(t.Nanosecond()) * time.Nanosecond)

// 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 {
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc).Add(-time.Second)

// Wrapped around.
if t.Month() == time.December {
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) {
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc).Add(-time.Second)
date := time.Date(t.Year(), t.Month()+1, 1, 0, 0, 0, 0, loc).AddDate(0, 0, -1)
if t.Day() == date.Day() {
goto WRAP
}
}

for 1<<uint(t.Hour())&s.Hour == 0 {
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc).Add(-time.Second)

if t.Hour() == 23 {
goto WRAP
}
}

for 1<<uint(t.Minute())&s.Minute == 0 {
t = t.Truncate(time.Minute).Add(-time.Second)

if t.Minute() == 59 {
goto WRAP
}
}

for 1<<uint(t.Second())&s.Second == 0 {
t = t.Add(-time.Second)

if t.Second() == 59 {
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
132 changes: 132 additions & 0 deletions spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,138 @@ 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:45 2012"},
{"Mon Jul 9 14:59 2012", "0 0/15 * * * *", "Mon Jul 9 14:45 2012"},
{"Mon Jul 9 14:45:01 2012", "0 0/15 * * * *", "Mon Jul 9 14:45 2012"},

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

// Wrap around days
{"Mon Jul 9 23:46 2012", "0 */15 * * * *", "Tue Jul 9 23:45 2012"},
{"Mon Jul 9 23:45 2012", "0 20-35/15 * * * *", "Tue Jul 9 23:35 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * * * *", "Tue Jul 9 23:35:50 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 * * *", "Tue Jul 9 23:35:50 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 2/2 * * *", "Tue Jul 9 22:35:50 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 10-12 * * *", "Tue Jul 9 12:35:50 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 */2 * *", "Thu Jul 9 23:35:50 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 2/2 * *", "Thu Jul 8 23:35:50 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 * *", "Wed Jul 9 23:35:50 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 10-20 * *", "Wed Jun 20 23:35:50 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 Jul *", "Wed Jul 9 23:35:50 2012"},

// Wrap around months
{"Mon Jul 9 23:35 2012", "0 0 0 9 Oct-Dec ?", "Thu Dec 9 00:00 2011"},
{"Mon Jul 9 23:35 2012", "0 0 0 */5 Apr,Aug,Oct Mon", "Mon Apr 30 00:00 2012"},
{"Mon Jul 9 23:35 2012", "0 0 0 */5 Oct Mon", "Mon Oct 31 00:00 2011"},

// Wrap around years
{"Mon Jul 9 23:35 2012", "0 0 0 * Nov Mon", "Mon Nov 28 00:00 2011"},
{"Mon Jul 9 23:35 2012", "0 0 0 * Nov Mon/2", "Wed Nov 30 00:00 2011"},

// Wrap around minute, hour, day, month, and year
{"Sat Jan 1 00:00 2012", "0 * * * * *", "Sat Jan 1 00:00:00 2012"},

// Leap year
{"Mon Feb 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2008"},

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

//// hourly job
//{"2012-03-11T00:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T01:00:00-0500"},
//{"2012-03-11T01:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T03:00:00-0400"},
//{"2012-03-11T03:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T04:00:00-0400"},
//{"2012-03-11T04:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T05:00:00-0400"},
//
//// hourly job using CRON_TZ
//{"2012-03-11T00:00:00-0500", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T01:00:00-0500"},
//{"2012-03-11T01:00:00-0500", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T03:00:00-0400"},
//{"2012-03-11T03:00:00-0400", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T04:00:00-0400"},
//{"2012-03-11T04:00:00-0400", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T05:00:00-0400"},
//
//// 1am nightly job
//{"2012-03-11T00:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-03-11T01:00:00-0500"},
//{"2012-03-11T01:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-03-12T01:00:00-0400"},
//
//// 2am nightly job (skipped)
//{"2012-03-11T00:00:00-0500", "TZ=America/New_York 0 0 2 * * ?", "2012-03-12T02:00:00-0400"},
//
//// Daylight savings time 2am EDT (-4) => 1am EST (-5)
//{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 30 2 04 Nov ?", "2012-11-04T02:30:00-0500"},
//{"2012-11-04T01:45:00-0400", "TZ=America/New_York 0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"},
//
//// hourly job
//{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T01:00:00-0400"},
//{"2012-11-04T01:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T01:00:00-0500"},
//{"2012-11-04T01:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T02:00:00-0500"},
//
//// 1am nightly job (runs twice)
//{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 1 * * ?", "2012-11-04T01:00:00-0400"},
//{"2012-11-04T01:00:00-0400", "TZ=America/New_York 0 0 1 * * ?", "2012-11-04T01:00:00-0500"},
//{"2012-11-04T01:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-11-05T01:00:00-0500"},
//
//// 2am nightly job
//{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 2 * * ?", "2012-11-04T02:00:00-0500"},
//{"2012-11-04T02:00:00-0500", "TZ=America/New_York 0 0 2 * * ?", "2012-11-05T02:00:00-0500"},
//
//// 3am nightly job
//{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 3 * * ?", "2012-11-04T03:00:00-0500"},
//{"2012-11-04T03:00:00-0500", "TZ=America/New_York 0 0 3 * * ?", "2012-11-05T03:00:00-0500"},
//
//// hourly job
//{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 * * * ?", "2012-11-04T01:00:00-0400"},
//{"TZ=America/New_York 2012-11-04T01:00:00-0400", "0 0 * * * ?", "2012-11-04T01:00:00-0500"},
//{"TZ=America/New_York 2012-11-04T01:00:00-0500", "0 0 * * * ?", "2012-11-04T02:00:00-0500"},
//
//// 1am nightly job (runs twice)
//{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 1 * * ?", "2012-11-04T01:00:00-0400"},
//{"TZ=America/New_York 2012-11-04T01:00:00-0400", "0 0 1 * * ?", "2012-11-04T01:00:00-0500"},
//{"TZ=America/New_York 2012-11-04T01:00:00-0500", "0 0 1 * * ?", "2012-11-05T01:00:00-0500"},
//
//// 2am nightly job
//{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 2 * * ?", "2012-11-04T02:00:00-0500"},
//{"TZ=America/New_York 2012-11-04T02:00:00-0500", "0 0 2 * * ?", "2012-11-05T02:00:00-0500"},
//
//// 3am nightly job
//{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 3 * * ?", "2012-11-04T03:00:00-0500"},
//{"TZ=America/New_York 2012-11-04T03:00:00-0500", "0 0 3 * * ?", "2012-11-05T03:00:00-0500"},
//
//// Unsatisfiable
//{"Mon Jul 9 23:35 2012", "0 0 0 30 Feb ?", ""},
//{"Mon Jul 9 23:35 2012", "0 0 0 31 Apr ?", ""},
//
//// Monthly job
//{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 3 3 * ?", "2012-12-03T03:00:00-0500"},
//
//// Test the scenario of DST resulting in midnight not being a valid time.
//// https://github.com/robfig/cron/issues/157
//{"2018-10-17T05:00:00-0400", "TZ=America/Sao_Paulo 0 0 9 10 * ?", "2018-11-10T06:00:00-0500"},
//{"2018-02-14T05:00:00-0500", "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