diff --git a/constantdelay.go b/constantdelay.go index cd6e7b1..ba079ec 100644 --- a/constantdelay.go +++ b/constantdelay.go @@ -1,6 +1,8 @@ package cron -import "time" +import ( + "time" +) // ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". // It does not support jobs more frequent than once a second. @@ -25,3 +27,10 @@ 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 previous time this would have been run +// This rounds to nearest second. +func (schedule ConstantDelaySchedule) Prev(t time.Time) time.Time { + t = t.Add(-time.Duration(t.Nanosecond()) * time.Nanosecond) + return t.Add(-schedule.Delay) +} diff --git a/constantdelay_test.go b/constantdelay_test.go index f43a58a..711bf14 100644 --- a/constantdelay_test.go +++ b/constantdelay_test.go @@ -52,3 +52,37 @@ func TestConstantDelayNext(t *testing.T) { } } } + +func TestConstantDelayPrev(t *testing.T) { + tests := []struct { + time string + delay time.Duration + expected string + }{ + // Simple cases + {"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 14:30 2012"}, + {"Mon Jul 9 14:59 2012", 15 * time.Minute, "Mon Jul 9 14:44 2012"}, + + // Wrap around days + {"Tue Jul 10 00:00 2012", 14 * time.Minute, "Mon Jul 9 23:46 2012"}, + {"Tue Jul 10 00:20:15 2012", 44*time.Minute + 24*time.Second, "Mon Jul 9 23:35:51 2012"}, + {"Thu Jul 11 01:20:15 2012", 25*time.Hour + 44*time.Minute + 24*time.Second, "Mon Jul 9 23:35:51 2012"}, + + // Wrap around minute, hour, day, month, and year + {"Tue Jan 1 00:00:00 2013", 15 * time.Second, "Mon Dec 31 23:59:45 2012"}, + + // Round to nearest second on the delay + {"Mon Jul 9 15:00 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 14:45 2012"}, + + // Round to second when calculating the prev time. + {"Mon Jul 9 15:00:00.005 2012", 15 * time.Minute, "Mon Jul 9 14:45:00 2012"}, + } + + for _, c := range tests { + actual := Every(c.delay).Prev(getTime(c.time)) + expected := getTime(c.expected) + if actual != expected { + t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.delay, expected, actual) + } + } +} diff --git a/cron.go b/cron.go index c7e9176..20da4c0 100644 --- a/cron.go +++ b/cron.go @@ -41,6 +41,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 previous time this schedule is activated, less than the given time. + Prev(time.Time) time.Time } // EntryID identifies an entry within a Cron instance diff --git a/cron_test.go b/cron_test.go index 36f06bf..0127fde 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 fa1e241..e9cd81f 100644 --- a/spec.go +++ b/spec.go @@ -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 { + // 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< 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< 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",