diff --git a/os/gcron/gcron_schedule.go b/os/gcron/gcron_schedule.go index 71da89d95..e25ce25f7 100644 --- a/os/gcron/gcron_schedule.go +++ b/os/gcron/gcron_schedule.go @@ -189,7 +189,9 @@ func parsePatternItem(item string, min int, max int, allowQuestionMark bool) (ma return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, item) } else { rangeMin = number - rangeMax = number + if len(intervalArray) == 1 { + rangeMax = number + } } } if len(rangeArray) == 2 { @@ -263,3 +265,115 @@ func (s *cronSchedule) meet(t time.Time) bool { return true } } + +// inspired by robfig/cron github.com/robfi/cron/v3@v3.0.0/spec.go +// https://github.com/robfig/cron/blob/master/spec_test.go +func (s *cronSchedule) Next(t time.Time) time.Time { + + if s.every != 0 { + + diff := t.Unix() - s.create + cnt := diff/s.every + 1 + return t.Add(time.Duration(cnt*s.every) * time.Second) + } + + // Start at the earliest possible time (the upcoming second). + t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) + + loc := t.Location() + added := false + yearLimit := t.Year() + 5 + // fmt.Printf("%+v \n", s) + +WRAP: + if t.Year() > yearLimit { + return t // who will care the job that run in five years later + } + + for !match(s.month, int(t.Month())) { + if !added { + added = true + t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc) + } + t = t.AddDate(0, 1, 0) + // need recheck + if t.Month() == time.January { + goto WRAP + } + } + + for !s.dayMatcher(t) { + if !added { + added = true + t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc) + } + t = t.AddDate(0, 0, 1) + + // 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("%v %v\n", t, t.Weekday()) + if t.Day() == 1 { + goto WRAP + } + } + + for !match(s.hour, t.Hour()) { + if !added { + added = true + t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc) + } + t = t.Add(time.Hour) + // need recheck + if t.Hour() == 0 { + goto WRAP + } + } + + for !match(s.minute, t.Minute()) { + if !added { + added = true + t = t.Truncate(time.Minute) + } + t = t.Add(1 * time.Minute) + + if t.Minute() == 0 { + goto WRAP + } + } + + for !match(s.second, t.Second()) { + if !added { + added = true + t = t.Truncate(time.Second) + } + t = t.Add(1 * time.Second) + + // fmt.Printf("%v\n", t) + if t.Second() == 0 { + goto WRAP + } + } + return t.In(loc) + +} + +func (s *cronSchedule) dayMatcher(t time.Time) bool { + + _, ok1 := s.day[t.Day()] + _, ok2 := s.week[int(t.Weekday())] + + return ok1 && ok2 +} + +func match(m map[int]struct{}, key int) bool { + _, ok := m[key] + return ok +} diff --git a/os/gcron/gcron_schedule_test.go b/os/gcron/gcron_schedule_test.go new file mode 100644 index 000000000..06c07d72c --- /dev/null +++ b/os/gcron/gcron_schedule_test.go @@ -0,0 +1,117 @@ +package gcron + +import ( + "strings" + "testing" + "time" + + "github.com/gogf/gf/v2/test/gtest" +) + +func TestSlash(t *testing.T) { + + runs := []struct { + spec string + expected map[int]struct{} + }{ + {"0 0 0 * Feb Mon/2", map[int]struct{}{1: struct{}{}, 3: struct{}{}, 5: struct{}{}}}, + {"0 0 0 * Feb *", map[int]struct{}{1: struct{}{}, 2: struct{}{}, 3: struct{}{}, 4: struct{}{}, 5: struct{}{}, 6: struct{}{}, 0: struct{}{}}}, + } + + gtest.C(t, func(t *gtest.T) { + for _, c := range runs { + sched, err := newSchedule(c.spec) + if err != nil { + t.Fatal(err) + + } + t.AssertEQ(sched.week, c.expected) + } + }) + +} + +func TestNext(t *testing.T) { + runs := []struct { + time, spec string + expected string + }{ + // Simple cases + {"Mon Jul 9 14:45 2012", "0 0/15 * * * *", "Mon Jul 9 15:00 2012"}, + {"Mon Jul 9 14:59 2012", "0 0/15 * * * *", "Mon Jul 9 15:00 2012"}, + {"Mon Jul 9 14:59:59 2012", "0 0/15 * * * *", "Mon Jul 9 15:00 2012"}, + + // Wrap around hours + {"Mon Jul 9 15:45 2012", "0 20-35/15 * * * *", "Mon Jul 9 16:20 2012"}, + + // Wrap around days + {"Mon Jul 9 23:46 2012", "0 */15 * * * *", "Tue Jul 10 00:00 2012"}, + {"Mon Jul 9 23:45 2012", "0 20-35/15 * * * *", "Tue Jul 10 00:20 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * * * *", "Tue Jul 10 00:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 * * *", "Tue Jul 10 01:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 10-12 * * *", "Tue Jul 10 10:20:15 2012"}, + + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 */2 * *", "Thu Jul 11 01:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 * *", "Wed Jul 10 00:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 Jul *", "Wed Jul 10 00:20:15 2012"}, + + // Wrap around months + {"Mon Jul 9 23:35 2012", "0 0 0 9 Apr-Oct ?", "Thu Aug 9 00:00 2012"}, + {"Mon Jul 9 23:35 2012", "0 0 0 */5 Apr,Aug,Oct Mon", "Mon Aug 6 00:00 2012"}, + {"Mon Jul 9 23:35 2012", "0 0 0 */5 Oct Mon", "Mon Oct 1 00:00 2012"}, + + // Wrap around years + {"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon", "Mon Feb 4 00:00 2013"}, + {"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon/2", "Fri Feb 1 00:00 2013"}, + + // Wrap around minute, hour, day, month, and year + {"Mon Dec 31 23:59:45 2012", "0 * * * * *", "Tue Jan 1 00:00:00 2013"}, + + // 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 := newSchedule(c.spec) + if err != nil { + t.Error(err) + continue + } + // fmt.Printf("%+v", sched) + actual := sched.Next(getTime(c.time)) + expected := getTime(c.expected) + if !(actual.Unix() == expected.Unix()) { + t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual) + } + } +} +func getTime(value string) time.Time { + if value == "" { + return time.Time{} + } + + var location = time.Local + if strings.HasPrefix(value, "TZ=") { + parts := strings.Fields(value) + loc, err := time.LoadLocation(parts[0][len("TZ="):]) + if err != nil { + panic("could not parse location:" + err.Error()) + } + location = loc + value = parts[1] + } + + var layouts = []string{ + "Mon Jan 2 15:04 2006", + "Mon Jan 2 15:04:05 2006", + } + for _, layout := range layouts { + if t, err := time.ParseInLocation(layout, value, location); err == nil { + return t + } + } + if t, err := time.ParseInLocation("2006-01-02T15:04:05-0700", value, location); err == nil { + return t + } + panic("could not parse time value " + value) +}