Skip to content

Commit 7b86320

Browse files
committed
feat: add schedule.Prev() from robfig/pull/361
1 parent 0885992 commit 7b86320

File tree

5 files changed

+264
-0
lines changed

5 files changed

+264
-0
lines changed

constantdelay.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,9 @@ func Every(duration time.Duration) ConstantDelaySchedule {
2525
func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {
2626
return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)
2727
}
28+
29+
// Prev returns the previous time this should be run.
30+
// It's not meaningful for ConstantDelaySchedule.
31+
func (schedule ConstantDelaySchedule) Prev(t time.Time) time.Time {
32+
return time.Time{}
33+
}

cron.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ type Schedule interface {
4141
// Next returns the next activation time, later than the given time.
4242
// Next is invoked initially, and then each time the job is run.
4343
Next(time.Time) time.Time
44+
45+
// Prev returns the previous activation time, earlier than the given time.
46+
Prev(time.Time) time.Time
4447
}
4548

4649
// EntryID identifies an entry within a Cron instance

cron_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,10 @@ func (*ZeroSchedule) Next(time.Time) time.Time {
546546
return time.Time{}
547547
}
548548

549+
func (*ZeroSchedule) Prev(time.Time) time.Time {
550+
return time.Time{}
551+
}
552+
549553
// Tests that job without time does not run
550554
func TestJobWithZeroTimeDoesNotRun(t *testing.T) {
551555
cron := newWithSeconds()

spec.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,129 @@ WRAP:
174174
return t.In(origLocation)
175175
}
176176

177+
// Prev returns the previous time this schedule should be activated, less than the given
178+
// time. If no time can be found to satisfy the schedule, return the zero time.
179+
func (s *SpecSchedule) Prev(t time.Time) time.Time {
180+
// General approach
181+
//
182+
// For Month, Day, Hour, Minute, Second:
183+
// Check if the time value matches. If yes, continue to the next field.
184+
// If the field doesn't match the schedule, then increment the field until it matches.
185+
// While incrementing the field, a wrap-around brings it back to the beginning
186+
// of the field list (since it is necessary to re-verify previous field
187+
// values)
188+
189+
// Convert the given time into the schedule's timezone, if one is specified.
190+
// Save the original timezone so we can convert back after we find a time.
191+
// Note that schedules without a time zone specified (time.Local) are treated
192+
// as local to the time provided.
193+
origLocation := t.Location()
194+
loc := s.Location
195+
if loc == time.Local {
196+
loc = t.Location()
197+
}
198+
if s.Location != time.Local {
199+
t = t.In(s.Location)
200+
}
201+
202+
// Start at the earliest possible time (the past second).
203+
t = t.Add(-1*time.Second + time.Duration(t.Nanosecond())*time.Nanosecond)
204+
205+
// This flag indicates whether a field has been incremented.
206+
added := false
207+
208+
// If no time is found within five years, return zero.
209+
yearLimit := t.Year() - 5
210+
211+
WRAP:
212+
if t.Year() < yearLimit {
213+
return time.Time{}
214+
}
215+
216+
// Find the first applicable month.
217+
// If it's this month, then do nothing.
218+
for 1<<uint(t.Month())&s.Month == 0 {
219+
cur := t.Month()
220+
// If we have to sub a month, reset the other parts to 0.
221+
if !added {
222+
added = true
223+
}
224+
// Otherwise, set the date at the beginning (since the current time is irrelevant).
225+
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc)
226+
t = t.Add(-1 * time.Second)
227+
228+
// Wrapped around.
229+
if t.Month() > cur {
230+
goto WRAP
231+
}
232+
}
233+
234+
// Now get a day in that month.
235+
//
236+
// NOTE: This causes issues for daylight savings regimes where midnight does
237+
// not exist. For example: Sao Paulo has DST that transforms midnight on
238+
// 11/3 into 1am. Handle that by noticing when the Hour ends up != 0.
239+
for !dayMatches(s, t) {
240+
cur := t.Day()
241+
if !added {
242+
added = true
243+
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
244+
t = t.Add(-1 * time.Second)
245+
} else {
246+
t = t.AddDate(0, 0, -1)
247+
}
248+
249+
// Wrapped around.
250+
if t.Day() > cur {
251+
goto WRAP
252+
}
253+
}
254+
255+
for 1<<uint(t.Hour())&s.Hour == 0 {
256+
cur := t.Hour()
257+
if !added {
258+
added = true
259+
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc)
260+
t = t.Add(-1 * time.Second)
261+
} else {
262+
t = t.Add(-1 * time.Hour)
263+
}
264+
if t.Hour() > cur {
265+
goto WRAP
266+
}
267+
}
268+
269+
for 1<<uint(t.Minute())&s.Minute == 0 {
270+
cur := t.Minute()
271+
if !added {
272+
added = true
273+
t = t.Truncate(time.Minute)
274+
t = t.Add(-1 * time.Second)
275+
} else {
276+
t = t.Add(-1 * time.Minute)
277+
}
278+
279+
if t.Minute() > cur {
280+
goto WRAP
281+
}
282+
}
283+
284+
for 1<<uint(t.Second())&s.Second == 0 {
285+
cur := t.Second()
286+
if !added {
287+
added = true
288+
t = t.Truncate(time.Second)
289+
}
290+
t = t.Add(-1 * time.Second)
291+
292+
if t.Second() > cur {
293+
goto WRAP
294+
}
295+
}
296+
297+
return t.In(origLocation)
298+
}
299+
177300
// dayMatches returns true if the schedule's day-of-week and day-of-month
178301
// restrictions are satisfied by the given time.
179302
func dayMatches(s *SpecSchedule, t time.Time) bool {

spec_test.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,134 @@ func TestNext(t *testing.T) {
199199
}
200200
}
201201

202+
func TestPrev(t *testing.T) {
203+
runs := []struct {
204+
time, spec string
205+
expected string
206+
}{
207+
// Simple cases
208+
{"Mon Jul 9 14:45 2012", "0 0/15 * * * *", "Mon Jul 9 14:30 2012"},
209+
{"Mon Jul 9 14:01 2012", "0 0/15 * * * *", "Mon Jul 9 14:00 2012"},
210+
{"Mon Jul 9 14:00:01 2012", "0 0/15 * * * *", "Mon Jul 9 14:00 2012"},
211+
212+
// Wrap around hours
213+
{"Mon Jul 9 15:20 2012", "0 20-35/15 * * * *", "Mon Jul 9 14:35 2012"},
214+
215+
// Wrap around days
216+
{"Mon Jul 9 00:00 2012", "0 */15 * * * *", "Sun Jul 8 23:45 2012"},
217+
{"Mon Jul 9 00:00 2012", "0 20-35/15 * * * *", "Sun Jul 8 23:35 2012"},
218+
{"Mon Jul 9 00:15:51 2012", "15/35 20-35/15 * * * *", "Sun Jul 8 23:35:50 2012"},
219+
{"Mon Jul 9 00:15:51 2012", "15/35 20-35/15 0/2 * * *", "Sun Jul 8 22:35:50 2012"},
220+
{"Mon Jul 9 00:15:51 2012", "15/35 20-35/15 10-12 * * *", "Sun Jul 8 12:35:50 2012"},
221+
222+
{"Mon Jul 9 00:15:51 2012", "15/35 20-35/15 0/2 */2 * *", "Sat Jul 7 22:35:50 2012"},
223+
{"Mon Jul 9 00:15:51 2012", "15/35 20-35/15 * 8-20 * *", "Sun Jul 8 23:35:50 2012"},
224+
{"Mon Jul 9 00:15:51 2012", "15/35 20-35/15 * 8-20 Jul *", "Sun Jul 8 23:35:50 2012"},
225+
226+
// Wrap around months
227+
{"Mon Jul 9 00:15 2012", "0 0 0 10 Apr-Oct ?", "Thu Jun 10 00:00 2012"},
228+
{"Mon Jul 9 00:15 2012", "0 0 0 */5 Apr,Jun Mon", "Tue Jun 26 00:00 2012"},
229+
{"Mon Jul 9 00:15 2012", "0 0 0 */5 Apr Mon", "Mon Apr 30 00:00 2012"},
230+
231+
// Wrap around years
232+
{"Mon Jul 9 00:15 2012", "0 0 0 * Aug Mon", "Mon Aug 29 00:00 2011"},
233+
{"Mon Jul 9 00:15 2012", "0 0 0 * Aug Mon/2", "Wed Aug 31 00:00 2011"},
234+
235+
// Wrap around minute, hour, day, month, and year
236+
{"Sun Jan 1 00:00:00 2012", "0 * * * * *", "Sat Dec 31 23:59:00 2011"},
237+
238+
// Leap year
239+
{"Mon Jul 9 00:15 2011", "0 0 0 29 Feb ?", "Fri Feb 29 00:00 2008"},
240+
241+
// Daylight savings time 2am EST (-5) -> 3am EDT (-4)
242+
{"2012-03-11T04:00:00-0400", "TZ=America/New_York 0 30 2 11 Mar ?", "2011-03-11T02:30:00-0500"},
243+
244+
// hourly job
245+
{"2012-03-11T01:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T00:00:00-0500"},
246+
{"2012-03-11T03:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T01:00:00-0500"},
247+
{"2012-03-11T04:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T03:00:00-0400"},
248+
{"2012-03-11T05:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T04:00:00-0400"},
249+
250+
// hourly job using CRON_TZ
251+
{"2012-03-11T01:00:00-0500", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T00:00:00-0500"},
252+
{"2012-03-11T03:00:00-0400", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T01:00:00-0500"},
253+
{"2012-03-11T04:00:00-0400", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T03:00:00-0400"},
254+
{"2012-03-11T05:00:00-0400", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T04:00:00-0400"},
255+
256+
// 1am nightly job
257+
{"2012-03-11T04:00:00-0400", "TZ=America/New_York 0 0 1 * * ?", "2012-03-11T01:00:00-0500"},
258+
{"2012-03-12T04:00:00-0400", "TZ=America/New_York 0 0 1 * * ?", "2012-03-12T01:00:00-0400"},
259+
260+
// 2am nightly job (skipped)
261+
{"2012-03-11T04:00:00-0400", "TZ=America/New_York 0 0 2 * * ?", "2012-03-10T02:00:00-0500"},
262+
263+
// Daylight savings time 2am EDT (-4) => 1am EST (-5)
264+
{"2012-11-04T01:45:00-0500", "TZ=America/New_York 0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"},
265+
{"2012-11-04T01:45:00-0400", "TZ=America/New_York 0 30 1 04 Nov ?", "2012-11-04T01:30:00-0400"},
266+
267+
// hourly job
268+
{"2012-11-04T01:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T00:00:00-0400"},
269+
{"2012-11-04T01:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T01:00:00-0400"},
270+
{"2012-11-04T02:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T01:00:00-0500"},
271+
272+
// 1am nightly job (runs twice)
273+
{"2012-11-04T01:00:00-0400", "TZ=America/New_York 0 0 1 * * ?", "2012-11-03T01:00:00-0400"},
274+
{"2012-11-04T01:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-11-04T01:00:00-0400"},
275+
{"2012-11-04T05:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-11-04T01:00:00-0500"},
276+
277+
// 2am nightly job
278+
{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 2 * * ?", "2012-11-03T02:00:00-0400"},
279+
{"2012-11-04T05:00:00-0500", "TZ=America/New_York 0 0 2 * * ?", "2012-11-04T02:00:00-0500"},
280+
281+
// 3am nightly job
282+
{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 3 * * ?", "2012-11-03T03:00:00-0400"},
283+
{"2012-11-04T05:00:00-0500", "TZ=America/New_York 0 0 3 * * ?", "2012-11-04T03:00:00-0500"},
284+
285+
// hourly job
286+
{"TZ=America/New_York 2012-11-04T01:00:00-0400", "0 0 * * * ?", "2012-11-04T00:00:00-0400"},
287+
{"TZ=America/New_York 2012-11-04T01:00:00-0500", "0 0 * * * ?", "2012-11-04T01:00:00-0400"},
288+
{"TZ=America/New_York 2012-11-04T02:00:00-0500", "0 0 * * * ?", "2012-11-04T01:00:00-0500"},
289+
290+
// 1am nightly job (runs twice)
291+
{"TZ=America/New_York 2012-11-04T01:00:00-0400", "0 0 1 * * ?", "2012-11-03T01:00:00-0400"},
292+
{"TZ=America/New_York 2012-11-04T01:00:00-0500", "0 0 1 * * ?", "2012-11-04T01:00:00-0400"},
293+
{"TZ=America/New_York 2012-11-04T05:00:00-0500", "0 0 1 * * ?", "2012-11-04T01:00:00-0500"},
294+
295+
// 2am nightly job
296+
{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 2 * * ?", "2012-11-03T02:00:00-0400"},
297+
{"TZ=America/New_York 2012-11-04T05:00:00-0500", "0 0 2 * * ?", "2012-11-04T02:00:00-0500"},
298+
299+
// 3am nightly job
300+
{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 3 * * ?", "2012-11-03T03:00:00-0400"},
301+
{"TZ=America/New_York 2012-11-04T05:00:00-0500", "0 0 3 * * ?", "2012-11-04T03:00:00-0500"},
302+
303+
// Unsatisfiable
304+
{"Mon Jul 9 00:15 2012", "0 0 0 30 Feb ?", ""},
305+
{"Mon Jul 9 00:15 2012", "0 0 0 31 Apr ?", ""},
306+
307+
// Monthly job
308+
{"TZ=America/New_York 2012-12-01T00:00:00-0500", "0 0 3 3 * ?", "2012-11-03T03:00:00-0400"},
309+
310+
// Test the scenario of DST resulting in midnight not being a valid time.
311+
// https://github.com/robfig/cron/issues/157
312+
{"2018-12-07T05:00:00-0500", "TZ=America/Sao_Paulo 0 0 9 10 * ?", "2018-11-10T06:00:00-0500"},
313+
{"2018-03-14T05:00:00-0400", "TZ=America/Sao_Paulo 0 0 9 22 * ?", "2018-02-22T07:00:00-0500"},
314+
}
315+
316+
for _, c := range runs {
317+
sched, err := secondParser.Parse(c.spec)
318+
if err != nil {
319+
t.Error(err)
320+
continue
321+
}
322+
actual := sched.Prev(getTime(c.time))
323+
expected := getTime(c.expected)
324+
if !actual.Equal(expected) {
325+
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual)
326+
}
327+
}
328+
}
329+
202330
func TestErrors(t *testing.T) {
203331
invalidSpecs := []string{
204332
"xyz",

0 commit comments

Comments
 (0)