Skip to content

Commit

Permalink
add previous
Browse files Browse the repository at this point in the history
  • Loading branch information
lvjing2 authored and youji.zzl committed Aug 2, 2019
1 parent e843a09 commit 264fe75
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 1 deletion.
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)
}

// Previous returns the latest time this should be run.
// This rounds so that the latest activation time will be on the second.
func (schedule ConstantDelaySchedule) Previous(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
// Previous returns the next activation time, smaller or equal with the given time.
// Previous is invoked initially, and then each time the job is run.
Previous(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) Previous(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)
}

// Previous 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) Previous(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 TestPrevious(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.Previous(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 264fe75

Please sign in to comment.