diff --git a/constantdelay.go b/constantdelay.go index cd6e7b1b..196f6ba0 100644 --- a/constantdelay.go +++ b/constantdelay.go @@ -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) +} diff --git a/cron.go b/cron.go index f6e451db..f71d33e7 100644 --- a/cron.go +++ b/cron.go @@ -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 diff --git a/cron_test.go b/cron_test.go index 36f06bf7..0127fdef 100644 --- a/cron_test.go +++ b/cron_test.go @@ -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() diff --git a/spec.go b/spec.go index fa1e241e..68c7b8af 100644 --- a/spec.go +++ b/spec.go @@ -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. @@ -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< 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",