97 lines
2.2 KiB
Go
97 lines
2.2 KiB
Go
package scheduler
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Expression struct{ fields [5]field }
|
|
type field map[int]bool
|
|
|
|
var bounds = [5][2]int{{0, 59}, {0, 23}, {1, 31}, {1, 12}, {0, 6}}
|
|
|
|
func Parse(spec string) (Expression, error) {
|
|
parts := strings.Fields(spec)
|
|
if len(parts) != 5 {
|
|
return Expression{}, fmt.Errorf("cron requires five fields")
|
|
}
|
|
var expression Expression
|
|
for i, part := range parts {
|
|
parsed, err := parseField(part, bounds[i][0], bounds[i][1])
|
|
if err != nil {
|
|
return Expression{}, fmt.Errorf("cron field %d: %w", i+1, err)
|
|
}
|
|
expression.fields[i] = parsed
|
|
}
|
|
return expression, nil
|
|
}
|
|
|
|
func parseField(value string, min, max int) (field, error) {
|
|
result := field{}
|
|
for _, segment := range strings.Split(value, ",") {
|
|
step := 1
|
|
base := segment
|
|
if strings.Contains(segment, "/") {
|
|
parts := strings.Split(segment, "/")
|
|
if len(parts) != 2 {
|
|
return nil, fmt.Errorf("invalid step %q", segment)
|
|
}
|
|
base = parts[0]
|
|
var err error
|
|
step, err = strconv.Atoi(parts[1])
|
|
if err != nil || step < 1 {
|
|
return nil, fmt.Errorf("invalid step %q", parts[1])
|
|
}
|
|
}
|
|
start, end := min, max
|
|
if base != "*" {
|
|
if strings.Contains(base, "-") {
|
|
parts := strings.Split(base, "-")
|
|
if len(parts) != 2 {
|
|
return nil, fmt.Errorf("invalid range %q", base)
|
|
}
|
|
start, _ = strconv.Atoi(parts[0])
|
|
end, _ = strconv.Atoi(parts[1])
|
|
} else {
|
|
var err error
|
|
start, err = strconv.Atoi(base)
|
|
end = start
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid value %q", base)
|
|
}
|
|
}
|
|
}
|
|
if start < min || end > max || start > end {
|
|
return nil, fmt.Errorf("value outside %d-%d", min, max)
|
|
}
|
|
for n := start; n <= end; n += step {
|
|
result[n] = true
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (e Expression) Matches(t time.Time) bool {
|
|
values := [5]int{t.Minute(), t.Hour(), t.Day(), int(t.Month()), int(t.Weekday())}
|
|
for i, value := range values {
|
|
if !e.fields[i][value] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (e Expression) Next(after time.Time) time.Time {
|
|
t := after.Truncate(time.Minute).Add(time.Minute)
|
|
limit := t.AddDate(2, 0, 0)
|
|
for t.Before(limit) {
|
|
if e.Matches(t) {
|
|
return t
|
|
}
|
|
t = t.Add(time.Minute)
|
|
}
|
|
return time.Time{}
|
|
}
|