package scheduler import ( "fmt" "strconv" "strings" "time" ) type CronExpr struct { Minute []int Hour []int DayOfMonth []int Month []int DayOfWeek []int } func ParseCron(expr string) (*CronExpr, error) { parts := strings.Fields(expr) if len(parts) != 5 { return nil, fmt.Errorf("expected 5 fields, got %d", len(parts)) } minute, err := parseField(parts[0], 0, 59) if err != nil { return nil, fmt.Errorf("minute: %w", err) } hour, err := parseField(parts[1], 0, 23) if err != nil { return nil, fmt.Errorf("hour: %w", err) } dom, err := parseField(parts[2], 1, 31) if err != nil { return nil, fmt.Errorf("day of month: %w", err) } month, err := parseField(parts[3], 1, 12) if err != nil { return nil, fmt.Errorf("month: %w", err) } dow, err := parseField(parts[4], 0, 6) if err != nil { return nil, fmt.Errorf("day of week: %w", err) } return &CronExpr{ Minute: minute, Hour: hour, DayOfMonth: dom, Month: month, DayOfWeek: dow, }, nil } func parseField(field string, min, max int) ([]int, error) { if field == "*" { var vals []int for i := min; i <= max; i++ { vals = append(vals, i) } return vals, nil } var result []int parts := strings.Split(field, ",") for _, part := range parts { if strings.Contains(part, "/") { stepParts := strings.Split(part, "/") if len(stepParts) != 2 { return nil, fmt.Errorf("invalid step: %s", part) } rangePart := stepParts[0] step, err := strconv.Atoi(stepParts[1]) if err != nil { return nil, fmt.Errorf("invalid step value: %s", stepParts[1]) } var start, end int if rangePart == "*" { start, end = min, max } else if strings.Contains(rangePart, "-") { rp := strings.Split(rangePart, "-") if len(rp) != 2 { return nil, fmt.Errorf("invalid range: %s", rangePart) } start, _ = strconv.Atoi(rp[0]) end, _ = strconv.Atoi(rp[1]) } else { v, _ := strconv.Atoi(rangePart) start, end = v, v } for i := start; i <= end; i += step { result = append(result, i) } } else if strings.Contains(part, "-") { rp := strings.Split(part, "-") if len(rp) != 2 { return nil, fmt.Errorf("invalid range: %s", part) } start, _ := strconv.Atoi(rp[0]) end, _ := strconv.Atoi(rp[1]) for i := start; i <= end; i++ { result = append(result, i) } } else { v, err := strconv.Atoi(part) if err != nil { return nil, fmt.Errorf("invalid value: %s", part) } if v < min || v > max { return nil, fmt.Errorf("value %d out of range [%d,%d]", v, min, max) } result = append(result, v) } } return result, nil } func (c *CronExpr) Matches(t time.Time) bool { if !contains(c.Minute, t.Minute()) { return false } if !contains(c.Hour, t.Hour()) { return false } if !contains(c.DayOfMonth, t.Day()) { return false } if !contains(c.Month, int(t.Month())) { return false } if !contains(c.DayOfWeek, int(t.Weekday())) { return false } return true } func contains(slice []int, val int) bool { for _, v := range slice { if v == val { return true } } return false } func NextRun(expr *CronExpr, from time.Time) time.Time { for i := 1; i <= 525600; i++ { t := from.Add(time.Duration(i) * time.Minute) if expr.Matches(t) { return t } } return from.AddDate(0, 0, 1) }