From c68523a37e3bc60b936db9df7d9a3f5500a6d427 Mon Sep 17 00:00:00 2001 From: Julie Vogelman Date: Mon, 31 Jan 2022 13:26:52 -0800 Subject: [PATCH 01/10] adding logic for SpecSchedule.Prev() to find previous trigger time based on time passed in --- spec.go | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/spec.go b/spec.go index fa1e241e..c56ba4d9 100644 --- a/spec.go +++ b/spec.go @@ -174,6 +174,117 @@ 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) + + // This flag indicates whether a field has been decremented. + //added := false + + // 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) + } + } + + if t.Day() == lastDayOfMonth(t.Month()) { + goto WRAP + } + } + + for 1< Date: Mon, 31 Jan 2022 15:35:28 -0800 Subject: [PATCH 02/10] remove DST logic from Prev() which seems to be unnecessary (and was also copy/pasted in error; also add test cases --- spec.go | 33 ++++++++++++++++++---------- spec_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/spec.go b/spec.go index c56ba4d9..cfc75a74 100644 --- a/spec.go +++ b/spec.go @@ -1,6 +1,9 @@ package cron -import "time" +import ( + "fmt" + "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. @@ -100,8 +103,10 @@ WRAP: added = true // Otherwise, set the date at the beginning (since the current time is irrelevant). t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc) + fmt.Printf("t: %v, added was false (month)\n", t) } t = t.AddDate(0, 1, 0) + fmt.Printf("t: %v, added month\n", t) // Wrapped around. if t.Month() == time.January { @@ -118,8 +123,10 @@ WRAP: if !added { added = true t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc) + fmt.Printf("t: %v, added was false (day)\n", t) } t = t.AddDate(0, 0, 1) + fmt.Printf("t: %v, added day\n", t) // 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 { @@ -139,8 +146,10 @@ WRAP: if !added { added = true t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc) + fmt.Printf("t: %v, added was false (hour)\n", t) } t = t.Add(1 * time.Hour) + fmt.Printf("t: %v, added hour\n", t) if t.Hour() == 0 { goto WRAP @@ -151,8 +160,10 @@ WRAP: if !added { added = true t = t.Truncate(time.Minute) + fmt.Printf("t: %v, added was false (minute)\n", t) } t = t.Add(1 * time.Minute) + fmt.Printf("t: %v, added minute\n", t) if t.Minute() == 0 { goto WRAP @@ -163,8 +174,10 @@ WRAP: if !added { added = true t = t.Truncate(time.Second) + fmt.Printf("t: %v, added was false (second)\n", t) } t = t.Add(1 * time.Second) + fmt.Printf("t: %v, added second\n", t) if t.Second() == 0 { goto WRAP @@ -214,6 +227,7 @@ WRAP: // TODO: is there a case in which we don't want to set this to the last possible second of that month? t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc) t = t.Add(-1 * time.Second) + //fmt.Printf("t: %v, last second of previous month\n", t) // Wrapped around. if t.Month() == time.December { @@ -232,19 +246,12 @@ WRAP: //t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc) }*/ // 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) t = t.Add(-1 * time.Second) - // 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) - } - } + //fmt.Printf("t: %v, last second of previous day\n", t) - if t.Day() == lastDayOfMonth(t.Month()) { + if saveMonth != t.Month() { goto WRAP } } @@ -257,6 +264,7 @@ WRAP: // 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) + //fmt.Printf("t: %v, last second of previous hour\n", t) if t.Hour() == 23 { goto WRAP @@ -267,6 +275,7 @@ WRAP: // 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) + //fmt.Printf("t: %v, last second of previous minute\n", t) if t.Minute() == 59 { goto WRAP @@ -276,12 +285,14 @@ WRAP: for 1< Date: Mon, 31 Jan 2022 16:15:28 -0800 Subject: [PATCH 03/10] adding more tests --- spec_test.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/spec_test.go b/spec_test.go index f31ae5b5..4715d7c1 100644 --- a/spec_test.go +++ b/spec_test.go @@ -230,19 +230,32 @@ func TestPrev(t *testing.T) { 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"}, + {"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"}, + {"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"}, + {"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 + {"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"}, } for _, c := range runs { From a20ce72725de09caf25f2c631303ded5745a7f53 Mon Sep 17 00:00:00 2001 From: Julie Vogelman Date: Tue, 1 Feb 2022 08:28:57 -0800 Subject: [PATCH 04/10] clean up log lines --- spec.go | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/spec.go b/spec.go index cfc75a74..de0935e2 100644 --- a/spec.go +++ b/spec.go @@ -103,10 +103,8 @@ WRAP: added = true // Otherwise, set the date at the beginning (since the current time is irrelevant). t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc) - fmt.Printf("t: %v, added was false (month)\n", t) } t = t.AddDate(0, 1, 0) - fmt.Printf("t: %v, added month\n", t) // Wrapped around. if t.Month() == time.January { @@ -123,10 +121,8 @@ WRAP: if !added { added = true t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc) - fmt.Printf("t: %v, added was false (day)\n", t) } t = t.AddDate(0, 0, 1) - fmt.Printf("t: %v, added day\n", t) // 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 { @@ -146,10 +142,8 @@ WRAP: if !added { added = true t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc) - fmt.Printf("t: %v, added was false (hour)\n", t) } t = t.Add(1 * time.Hour) - fmt.Printf("t: %v, added hour\n", t) if t.Hour() == 0 { goto WRAP @@ -160,10 +154,8 @@ WRAP: if !added { added = true t = t.Truncate(time.Minute) - fmt.Printf("t: %v, added was false (minute)\n", t) } t = t.Add(1 * time.Minute) - fmt.Printf("t: %v, added minute\n", t) if t.Minute() == 0 { goto WRAP @@ -174,10 +166,8 @@ WRAP: if !added { added = true t = t.Truncate(time.Second) - fmt.Printf("t: %v, added was false (second)\n", t) } t = t.Add(1 * time.Second) - fmt.Printf("t: %v, added second\n", t) if t.Second() == 0 { goto WRAP @@ -204,9 +194,6 @@ func (s *SpecSchedule) Prev(t time.Time) time.Time { // Start at the previous second t = t.Add(-1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) - // This flag indicates whether a field has been decremented. - //added := false - // If no time is found within five years, return zero. yearLimit := t.Year() - 5 @@ -218,16 +205,9 @@ WRAP: // Find the first applicable month. // If it's this month, then do nothing. for 1< Date: Tue, 1 Feb 2022 08:29:30 -0800 Subject: [PATCH 05/10] clean up log lines --- spec.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec.go b/spec.go index de0935e2..85cb1d68 100644 --- a/spec.go +++ b/spec.go @@ -1,7 +1,6 @@ package cron import ( - "fmt" "time" ) @@ -260,7 +259,6 @@ WRAP: } } - fmt.Println(t) return t.In(origLocation) } From 3f910138def7757e4324bc1b53150c9f47b0a394 Mon Sep 17 00:00:00 2001 From: Julie Vogelman Date: Tue, 1 Feb 2022 08:32:04 -0800 Subject: [PATCH 06/10] cleaning up test code and adding more tests --- spec_test.go | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/spec_test.go b/spec_test.go index 4715d7c1..1b3f4f51 100644 --- a/spec_test.go +++ b/spec_test.go @@ -71,30 +71,6 @@ func TestActivation(t *testing.T) { } } -func TestNextTemp(t *testing.T) { - runs := []struct { - time, spec string - expected string - }{ - - // Leap year - {"Mon Jul 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"}, - } - - for _, c := range runs { - sched, err := secondParser.Parse(c.spec) - if err != nil { - t.Error(err) - continue - } - actual := sched.Next(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 TestNext(t *testing.T) { runs := []struct { time, spec string @@ -253,9 +229,21 @@ func TestPrev(t *testing.T) { // 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"}, + + {"2018-10-17T05:00:00-0400", "TZ=America/Sao_Paulo 0 0 9 10 * ?", "2018-11-10T06:00:00-0500"}, } for _, c := range runs { From 1e45f95e9fee953ca85273a941b4fd8339aa9b78 Mon Sep 17 00:00:00 2001 From: Julie Vogelman Date: Tue, 1 Feb 2022 08:40:32 -0800 Subject: [PATCH 07/10] remove unnecessary change to import statement --- spec.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec.go b/spec.go index 85cb1d68..fe3e298b 100644 --- a/spec.go +++ b/spec.go @@ -1,8 +1,6 @@ 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. From 9cdd9cfce85986d04b51858427f0e079a84fa4c8 Mon Sep 17 00:00:00 2001 From: Julie Vogelman Date: Tue, 1 Feb 2022 12:02:59 -0800 Subject: [PATCH 08/10] remove accidental copy/paste in test (probably still need to add in a test for Sao Paolo later, however Signed-off-by: Julie Vogelman --- spec_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec_test.go b/spec_test.go index 1b3f4f51..8d9974b8 100644 --- a/spec_test.go +++ b/spec_test.go @@ -242,8 +242,6 @@ func TestPrev(t *testing.T) { // Monthly job {"TZ=America/New_York 2012-12-03T00:00:00-0500", "0 0 3 3 * ?", "2012-11-03T03:00:00-0400"}, - - {"2018-10-17T05:00:00-0400", "TZ=America/Sao_Paulo 0 0 9 10 * ?", "2018-11-10T06:00:00-0500"}, } for _, c := range runs { From 588c2c0f7d0e33237c76d667352b45224a560e42 Mon Sep 17 00:00:00 2001 From: Julie Vogelman Date: Tue, 1 Feb 2022 12:11:49 -0800 Subject: [PATCH 09/10] attempting to address the Sao Paolo issue through imitation of the Next() function Signed-off-by: Julie Vogelman --- spec.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spec.go b/spec.go index fe3e298b..e9cd81f8 100644 --- a/spec.go +++ b/spec.go @@ -221,6 +221,17 @@ WRAP: // 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() { From 7181f74c09e99418674d5ef1e438066a4b841a7a Mon Sep 17 00:00:00 2001 From: Julie Vogelman Date: Wed, 9 Mar 2022 22:32:35 -0800 Subject: [PATCH 10/10] Incorporating Prev() into the Schedule interface and thus into ConstantDelaySchedule struct Signed-off-by: Julie Vogelman --- constantdelay.go | 11 ++++++++++- constantdelay_test.go | 34 ++++++++++++++++++++++++++++++++++ cron.go | 3 +++ cron_test.go | 4 ++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/constantdelay.go b/constantdelay.go index cd6e7b1b..ba079ecc 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 f43a58ad..711bf14c 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 c7e91766..20da4c0a 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 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()