Extend cron patterns to support dow and dom
This commit is contained in:
parent
8eeb438473
commit
7d317f47ea
89
core/cron.go
89
core/cron.go
@ -13,6 +13,8 @@ func GetNextUpdate(lastUpdated time.Time, spec string) (nextUpdate time.Time, er
|
||||
nextUpdates, err = parseEverySpec(lastUpdated, spec[len("every "):])
|
||||
case strings.HasPrefix(spec, "at "):
|
||||
nextUpdates, err = parseAtSpec(lastUpdated, spec[len("at "):])
|
||||
case strings.HasPrefix(spec, "on "):
|
||||
nextUpdates, err = parseOnSpec(lastUpdated, spec[len("on "):])
|
||||
default:
|
||||
return time.Time{}, fmt.Errorf("unknown spec format: %v", spec)
|
||||
}
|
||||
@ -45,8 +47,7 @@ func parseEverySpec(base time.Time, everySpec string) (nextUpdates []time.Time,
|
||||
// Get the next instances of the at-spec times after the base time.
|
||||
// An at-spec is in the patterm HH:MM[,HH:MM,[...]].
|
||||
func parseAtSpec(base time.Time, atSpec string) (nextUpdates []time.Time, err error) {
|
||||
timeSpecs := strings.Split(atSpec, ",")
|
||||
for _, timeSpec := range timeSpecs {
|
||||
for _, timeSpec := range strings.Split(atSpec, ",") {
|
||||
var hour, minute int
|
||||
_, err = fmt.Sscanf(timeSpec, "%d:%d", &hour, &minute)
|
||||
if err != nil {
|
||||
@ -62,3 +63,87 @@ func parseAtSpec(base time.Time, atSpec string) (nextUpdates []time.Time, err er
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var weekdays = map[string]time.Weekday{
|
||||
"Sun": time.Sunday,
|
||||
"Mon": time.Monday,
|
||||
"Tue": time.Tuesday,
|
||||
"Wed": time.Wednesday,
|
||||
"Thu": time.Thursday,
|
||||
"Fri": time.Friday,
|
||||
"Sat": time.Saturday,
|
||||
}
|
||||
|
||||
// Get the next instances of the on-spec times after the base time.
|
||||
// An on-spec is in the pattern DOW[,DOW[...]] where DOW is an abbreviated weekday
|
||||
// or M/D[,M/D[...]] where M/D is a month and day.
|
||||
// As a special case, "*/N" matches the Nth day of every month.
|
||||
// An on-spec may be followed by an at-spec; otherwise, "at 00:00" is implied.
|
||||
func parseOnSpec(base time.Time, onSpec string) (nextUpdates []time.Time, err error) {
|
||||
type Date struct {
|
||||
Year int
|
||||
Month time.Month
|
||||
Day int
|
||||
}
|
||||
type Time struct {
|
||||
Hour int
|
||||
Minute int
|
||||
}
|
||||
|
||||
atSpec := "00:00"
|
||||
if on := strings.Index(onSpec, " at "); on > -1 {
|
||||
atSpec = onSpec[on+len(" at "):]
|
||||
onSpec = onSpec[:on]
|
||||
}
|
||||
var atTimes []Time
|
||||
for _, timeSpec := range strings.Split(atSpec, ",") {
|
||||
var hour, minute int
|
||||
_, err := fmt.Sscanf(timeSpec, "%d:%d", &hour, &minute)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse at-spec %s: %v", timeSpec, err)
|
||||
}
|
||||
atTimes = append(atTimes, Time{hour, minute})
|
||||
}
|
||||
|
||||
var dates []Date
|
||||
for _, daySpec := range strings.Split(onSpec, ",") {
|
||||
if weekday, ok := weekdays[daySpec]; ok {
|
||||
// For a weekday, add the next of that weekday 0-6 days ahead and 7-13 days ahead.
|
||||
// The first date ensures that we don't miss multiple updates on the same day of the week
|
||||
// (e.g. "on Sun at 06:00,18:00") and the second date ensures that we don't get stuck with
|
||||
// only a date in the past (e.g. "on Sun at 02:00" when base is Sun 03:00).
|
||||
daysForward := (int(weekday) - int(base.Weekday()) + 7) % 7 // value in [0,6]
|
||||
day0_6 := base.AddDate(0, 0, daysForward)
|
||||
dates = append(dates, Date{day0_6.Year(), day0_6.Month(), day0_6.Day()})
|
||||
day7_13 := base.AddDate(0, 0, daysForward+7)
|
||||
dates = append(dates, Date{day7_13.Year(), day7_13.Month(), day7_13.Day()})
|
||||
} else if strings.HasPrefix(daySpec, "*/") {
|
||||
// For every-month, add the date for the current month and the date for the next month.
|
||||
var day int
|
||||
_, err := fmt.Sscanf(daySpec, "*/%d", &day)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse month/day %s: %v", daySpec, err)
|
||||
}
|
||||
dates = append(dates, Date{base.Year(), base.Month(), day})
|
||||
nextMonth := base.AddDate(0, 1, 0)
|
||||
dates = append(dates, Date{nextMonth.Year(), nextMonth.Month(), day})
|
||||
} else {
|
||||
// For month/day, add the date for the base year and the next year.
|
||||
var month, day int
|
||||
_, err := fmt.Sscanf(daySpec, "%d/%d", &month, &day)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse month/day %s: %v", daySpec, err)
|
||||
}
|
||||
dates = append(dates, Date{base.Year(), time.Month(month), day})
|
||||
dates = append(dates, Date{base.Year() + 1, time.Month(month), day})
|
||||
}
|
||||
}
|
||||
|
||||
// Now, for each date, create a datetime based on the at-spec.
|
||||
for _, date := range dates {
|
||||
for _, atTime := range atTimes {
|
||||
nextUpdates = append(nextUpdates, time.Date(date.Year, date.Month, date.Day, atTime.Hour, atTime.Minute, 0, 0, base.Location()))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -7,103 +7,163 @@ import (
|
||||
)
|
||||
|
||||
func TestGetNextUpdate(t *testing.T) {
|
||||
tests := []struct {
|
||||
lastUpdated time.Time
|
||||
spec string
|
||||
expected time.Time
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
lastUpdated: time.Date(2020, 10, 22, 12, 0, 0, 0, time.UTC),
|
||||
spec: "every 1h#1",
|
||||
expected: time.Date(2020, 10, 22, 13, 0, 0, 0, time.UTC),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
lastUpdated: time.Date(2020, 10, 22, 12, 0, 5, 0, time.UTC),
|
||||
spec: "every 1h#2",
|
||||
expected: time.Date(2020, 10, 22, 13, 0, 0, 0, time.UTC),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
lastUpdated: time.Date(2020, 10, 22, 11, 0, 0, 0, time.UTC),
|
||||
spec: "every 2h30m#1",
|
||||
expected: time.Date(2020, 10, 22, 11, 30, 0, 0, time.UTC),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
lastUpdated: time.Date(2020, 10, 22, 11, 30, 0, 0, time.UTC),
|
||||
spec: "every 2h30m#2",
|
||||
expected: time.Date(2020, 10, 22, 14, 00, 0, 0, time.UTC),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
lastUpdated: time.Date(2020, 10, 22, 11, 30, 0, 0, time.UTC),
|
||||
spec: "every 3",
|
||||
expected: time.Time{},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
lastUpdated: time.Date(2020, 10, 22, 12, 0, 0, 0, time.UTC),
|
||||
spec: "at 14:00#1",
|
||||
expected: time.Date(2020, 10, 22, 14, 0, 0, 0, time.UTC),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
lastUpdated: time.Date(2020, 10, 22, 14, 0, 0, 0, time.UTC),
|
||||
spec: "at 14:00#2",
|
||||
expected: time.Date(2020, 10, 23, 14, 0, 0, 0, time.UTC),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
lastUpdated: time.Date(2020, 10, 22, 15, 0, 0, 0, time.UTC),
|
||||
spec: "at 14:00#3",
|
||||
expected: time.Date(2020, 10, 23, 14, 0, 0, 0, time.UTC),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
lastUpdated: time.Date(2020, 10, 22, 9, 0, 0, 0, time.UTC),
|
||||
spec: "at 10:00,15:00#1",
|
||||
expected: time.Date(2020, 10, 22, 10, 0, 0, 0, time.UTC),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
lastUpdated: time.Date(2020, 10, 22, 12, 0, 0, 0, time.UTC),
|
||||
spec: "at 10:00,15:00#2",
|
||||
expected: time.Date(2020, 10, 22, 15, 0, 0, 0, time.UTC),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
lastUpdated: time.Date(2020, 10, 22, 15, 0, 0, 0, time.UTC),
|
||||
spec: "at 10:00,15:00#3",
|
||||
expected: time.Date(2020, 10, 23, 10, 0, 0, 0, time.UTC),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
lastUpdated: time.Date(2020, 10, 22, 16, 0, 0, 0, time.UTC),
|
||||
spec: "at 10:00,15:00#4",
|
||||
expected: time.Date(2020, 10, 23, 10, 0, 0, 0, time.UTC),
|
||||
expectErr: false,
|
||||
},
|
||||
date := func(year int, month time.Month, day, hour, minute int) time.Time {
|
||||
return time.Date(year, month, day, hour, minute, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.spec, func(t *testing.T) {
|
||||
spec := test.spec
|
||||
expect := func(lastUpdated time.Time, spec string, expected time.Time) {
|
||||
t.Run(spec, func(t *testing.T) {
|
||||
if comment := strings.Index(spec, "#"); comment > -1 {
|
||||
spec = spec[:comment]
|
||||
}
|
||||
nextUpdate, err := GetNextUpdate(test.lastUpdated, spec)
|
||||
if test.expectErr && err == nil {
|
||||
t.Error("test did not fail as expected")
|
||||
nextUpdate, err := GetNextUpdate(lastUpdated, spec)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("error: %v", err)
|
||||
}
|
||||
if !nextUpdate.Equal(test.expected) {
|
||||
t.Errorf("\nexpected: %v\n got: %v", test.expected, nextUpdate)
|
||||
if !nextUpdate.Equal(expected) {
|
||||
t.Errorf("\nexpected: %v\n got: %v", expected, nextUpdate)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
expectFail := func(lastUpdated time.Time, spec string) {
|
||||
t.Run(spec, func(t *testing.T) {
|
||||
if comment := strings.Index(spec, "#"); comment > -1 {
|
||||
spec = spec[:comment]
|
||||
}
|
||||
_, err := GetNextUpdate(lastUpdated, spec)
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
expect(
|
||||
date(2020, 10, 22, 12, 0),
|
||||
"every 1h#1",
|
||||
date(2020, 10, 22, 13, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 22, 12, 1),
|
||||
"every 1h#2",
|
||||
date(2020, 10, 22, 13, 0),
|
||||
)
|
||||
|
||||
expect(
|
||||
date(2020, 10, 22, 11, 0),
|
||||
"every 2h30m#1",
|
||||
date(2020, 10, 22, 11, 30),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 22, 11, 30),
|
||||
"every 2h30m#2",
|
||||
date(2020, 10, 22, 14, 0),
|
||||
)
|
||||
|
||||
expectFail(
|
||||
date(2020, 10, 22, 11, 30),
|
||||
"every 3",
|
||||
)
|
||||
|
||||
expect(
|
||||
date(2020, 10, 22, 12, 0),
|
||||
"at 14:00#1",
|
||||
date(2020, 10, 22, 14, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 22, 14, 0),
|
||||
"at 14:00#2",
|
||||
date(2020, 10, 23, 14, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 22, 15, 0),
|
||||
"at 14:00#3",
|
||||
date(2020, 10, 23, 14, 0),
|
||||
)
|
||||
|
||||
expect(
|
||||
date(2020, 10, 22, 9, 0),
|
||||
"at 10:00,15:00#1",
|
||||
date(2020, 10, 22, 10, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 22, 12, 0),
|
||||
"at 10:00,15:00#2",
|
||||
date(2020, 10, 22, 15, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 22, 15, 0),
|
||||
"at 10:00,15:00#3",
|
||||
date(2020, 10, 23, 10, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 22, 16, 0),
|
||||
"at 10:00,15:00#4",
|
||||
date(2020, 10, 23, 10, 0),
|
||||
)
|
||||
|
||||
expect(
|
||||
date(2020, 10, 22, 6, 0),
|
||||
"on Sun#1",
|
||||
date(2020, 10, 25, 0, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 25, 0, 0),
|
||||
"on Sun#2",
|
||||
date(2020, 11, 1, 0, 0),
|
||||
)
|
||||
|
||||
expect(
|
||||
date(2020, 10, 19, 0, 0),
|
||||
"on Tue,Thu at 04:00#1",
|
||||
date(2020, 10, 20, 4, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 20, 3, 0),
|
||||
"on Tue,Thu at 04:00#2",
|
||||
date(2020, 10, 20, 4, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 20, 4, 0),
|
||||
"on Tue,Thu at 04:00#3",
|
||||
date(2020, 10, 22, 4, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 21, 4, 0),
|
||||
"on Tue,Thu at 04:00#4",
|
||||
date(2020, 10, 22, 4, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 22, 4, 0),
|
||||
"on Tue,Thu at 04:00#5",
|
||||
date(2020, 10, 27, 4, 0),
|
||||
)
|
||||
|
||||
expect(
|
||||
date(2020, 10, 21, 12, 0),
|
||||
"on 10/22 at 12:00#1",
|
||||
date(2020, 10, 22, 12, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 22, 10, 0),
|
||||
"on 10/22 at 12:00#2",
|
||||
date(2020, 10, 22, 12, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 22, 12, 0),
|
||||
"on 10/22 at 12:00#3",
|
||||
date(2021, 10, 22, 12, 0),
|
||||
)
|
||||
|
||||
expect(
|
||||
date(2020, 10, 21, 12, 0),
|
||||
"on */22#1",
|
||||
date(2020, 10, 22, 0, 0),
|
||||
)
|
||||
expect(
|
||||
date(2020, 10, 22, 0, 0),
|
||||
"on */22#2",
|
||||
date(2020, 11, 22, 0, 0),
|
||||
)
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user