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

feat: Prev() function derives the previous time a SpecSchedule would have triggered #437

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
97 changes: 97 additions & 0 deletions spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,103 @@ WRAP:
return t.In(origLocation)
}

// Prev returns the previous time this schedule is 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 {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it was wanted or not, but this function should be added to the Schedule interface for it to be accessible externally.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll add it to the interface and to the ConstantDelaySchedule struct.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, done

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There hasn't been any contribution to the repo since 2020 so let's hope that someone merges this! Thanks for implementing it though

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this change might be really beneficial for distributed systems (check if the previous job has been run). Is this project dead @robfig @juliev0?

// General approach is based on approach to Next() implementation

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 previous second
t = t.Add(-1*time.Second - 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 {
// set t to the last second of the previous month
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc)
t = t.Add(-1 * 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) {
// set t to the last second of the previous day
saveMonth := t.Month()
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)

// Notice if the hour is no longer midnight due to DST.
// Add an hour if it's 23, subtract an hour if it's 1.
if t.Hour() != 0 {
if t.Hour() > 12 {
t = t.Add(time.Duration(24-t.Hour()) * time.Hour)
} else {
t = t.Add(time.Duration(-t.Hour()) * time.Hour)
}
}

t = t.Add(-1 * time.Second)

if saveMonth != t.Month() {
goto WRAP
}
}

for 1<<uint(t.Hour())&s.Hour == 0 {
// set t to the last second of the previous hour
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc)
t = t.Add(-1 * time.Second)

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

for 1<<uint(t.Minute())&s.Minute == 0 {
// set t to the last second of the previous minute
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, loc)
t = t.Add(-1 * time.Second)

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

for 1<<uint(t.Second())&s.Second == 0 {
// set t to the previous second
t = t.Add(-1 * 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
61 changes: 61 additions & 0 deletions spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func TestNext(t *testing.T) {
time, spec string
expected string
}{

// Simple cases
{"Mon Jul 9 14:45 2012", "0 0/15 * * * *", "Mon Jul 9 15:00 2012"},
{"Mon Jul 9 14:59 2012", "0 0/15 * * * *", "Mon Jul 9 15:00 2012"},
Expand Down Expand Up @@ -199,6 +200,66 @@ func TestNext(t *testing.T) {
}
}

func TestPrev(t *testing.T) {
runs := []struct {
time, spec string
expected string
}{
// Simple cases
{"Mon Jul 9 15:00 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 15:01:59 2012", "0 0/15 * * * *", "Mon Jul 9 15:00 2012"},

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

// Wrap around days
{"Tue Jul 10 00:00 2012", "0 */15 * * * *", "Tue Jul 9 23:45 2012"},
{"Tue Jul 10 00:00 2012", "0 20-35/15 * * * *", "Tue Jul 9 23:35 2012"},

// Wrap around months
{"Mon Jul 9 09:35 2012", "0 0 12 9 Apr-Oct ?", "Sat Jun 9 12:00 2012"},

// Leap year
{"Mon Jul 9 23:35 2018", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"},

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

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

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

// 2am nightly job
{"2012-11-04T02:00:00-0500", "TZ=America/New_York 0 0 0 * * ?", "2012-11-04T00:00:00-0400"},
{"2012-11-05T02:00:00-0500", "TZ=America/New_York 0 0 2 * * ?", "2012-11-04T02: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-12-03T00:00:00-0500", "0 0 3 3 * ?", "2012-11-03T03:00:00-0400"},
}

for _, c := range runs {
sched, err := secondParser.Parse(c.spec)
if err != nil {
t.Error(err)
continue
}
specSchedule, _ := sched.(*SpecSchedule)

actual := specSchedule.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