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{} }