diff --git a/node/cron/at.go b/node/cron/at.go new file mode 100644 index 0000000..4f6ab8c --- /dev/null +++ b/node/cron/at.go @@ -0,0 +1,34 @@ +package cron + +import ( + "sort" + "time" +) + +// TimeListSchedule will run at the specify giving time. +type TimeListSchedule struct { + timeList []time.Time +} + +// At returns a crontab Schedule that activates every specify time. +func At(tl []time.Time) *TimeListSchedule { + sort.Slice(tl, func(i, j int) bool { return tl[i].Unix() < tl[j].Unix() }) + return &TimeListSchedule{ + timeList: tl, + } +} + +// Next returns the next time this should be run. +// This rounds so that the next activation time will be on the second. +func (schedule *TimeListSchedule) Next(t time.Time) time.Time { + cur := 0 + for cur < len(schedule.timeList) { + nextt := schedule.timeList[cur] + cur++ + if nextt.UnixNano() <= t.UnixNano() { + continue + } + return nextt + } + return time.Time{} +} diff --git a/node/cron/at_test.go b/node/cron/at_test.go new file mode 100644 index 0000000..fc10cd8 --- /dev/null +++ b/node/cron/at_test.go @@ -0,0 +1,89 @@ +package cron + +import ( + "testing" + "time" +) + +func TestTimeListNext(t *testing.T) { + tests := []struct { + startTime string + times []string + expected []string + }{ + // Simple cases + { + "2018-09-01 08:01:02", + []string{"2018-09-01 10:01:02"}, + []string{"2018-09-01 10:01:02"}, + }, + + // sort list + { + "2018-09-01 08:01:02", + []string{"2018-09-01 10:01:02", "2018-09-02 10:01:02"}, + []string{"2018-09-01 10:01:02", "2018-09-02 10:01:02"}, + }, + + // sort list with middle start time + { + "2018-09-01 10:11:02", + []string{"2018-09-01 10:01:02", "2018-09-02 10:01:02"}, + []string{"2018-09-02 10:01:02"}, + }, + + // unsorted list + { + "2018-07-01 08:01:02", + []string{"2018-09-01 10:01:00", "2018-08-01 10:00:00", "2018-09-01 10:00:00", "2018-08-02 10:01:02"}, + []string{"2018-08-01 10:00:00", "2018-08-02 10:01:02", "2018-09-01 10:00:00", "2018-09-01 10:01:00"}, + }, + + // unsorted list with middle start time + { + "2018-08-03 12:00:00", + []string{"2018-09-01 10:01:00", "2018-08-01 10:00:00", "2018-09-01 10:00:00", "2018-08-02 10:01:02"}, + []string{"2018-09-01 10:00:00", "2018-09-01 10:01:00"}, + }, + } + + for _, c := range tests { + tls := At(getAtTimes(c.times)) + nextTime := getAtTime(c.startTime) + for _, trun := range c.expected { + actual := tls.Next(nextTime) + expected := getAtTime(trun) + if actual != expected { + t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", + c.startTime, c.times, expected, actual) + } + nextTime = actual + } + if actual := tls.Next(nextTime); !actual.IsZero() { + t.Errorf("%s, \"%s\": next time should be zero, but got %v (actual)", + c.startTime, c.times, actual) + } + + } +} + +func getAtTime(value string) time.Time { + if value == "" { + panic("time string is empty") + } + + t, err := time.Parse("2006-01-02 15:04:05", value) + if err != nil { + panic(err) + } + + return t +} + +func getAtTimes(values []string) []time.Time { + tl := []time.Time{} + for _, v := range values { + tl = append(tl, getAtTime(v)) + } + return tl +} diff --git a/node/cron/parser.go b/node/cron/parser.go index b8a6d25..3bbd2a3 100644 --- a/node/cron/parser.go +++ b/node/cron/parser.go @@ -373,5 +373,21 @@ func parseDescriptor(descriptor string) (Schedule, error) { return Every(duration), nil } + const at = "@at " + if strings.HasPrefix(descriptor, at) { + tss := strings.Split(descriptor[len(at):], ",") + atls := make([]time.Time, 0, len(tss)) + for _, ts := range tss { + ts = strings.TrimSpace(ts) + att, err := time.ParseInLocation("2006-01-02 15:04:05", ts, time.Local) + if err != nil { + return nil, fmt.Errorf("Failed to parse time %s: %s", descriptor, err) + } + atls = append(atls, att) + } + + return At(atls), nil + } + return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor) }