package cron import ( "errors" "fmt" "strconv" "strings" "time" ) // schedule is a parsed five-field cron expression as sets. type schedule struct { min, hour, dom, mon, dow [64]bool domStar, dowStar bool // "*" in dom/dow changes the match rule reboot bool } var monthNames = map[string]int{"jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6, "jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12} var dayNames = map[string]int{"sun": 0, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6} var aliases = map[string]string{ "@yearly": "0 0 1 1 *", "@annually": "0 0 1 1 *", "@monthly": "0 0 1 * *", "@weekly": "0 0 * * 0", "@daily": "0 0 * * *", "@midnight": "0 0 * * *", "@hourly": "0 * * * *", } func parseSchedule(expr string) (*schedule, error) { expr = strings.TrimSpace(expr) if expr == "@reboot" { return &schedule{reboot: true}, nil } if a, ok := aliases[strings.ToLower(expr)]; ok { expr = a } f := strings.Fields(expr) if len(f) != 5 { return nil, errors.New("expected 5 fields") } s := &schedule{} var err error if s.min, err = parseField(f[0], 0, 59, nil); err != nil { return nil, fmt.Errorf("minute: %w", err) } if s.hour, err = parseField(f[1], 0, 23, nil); err != nil { return nil, fmt.Errorf("hour: %w", err) } if s.dom, err = parseField(f[2], 1, 31, nil); err != nil { return nil, fmt.Errorf("day of month: %w", err) } if s.mon, err = parseField(f[3], 1, 12, monthNames); err != nil { return nil, fmt.Errorf("month: %w", err) } if s.dow, err = parseField(f[4], 0, 7, dayNames); err != nil { return nil, fmt.Errorf("day of week: %w", err) } if s.dow[7] { // 7 == Sunday s.dow[0] = true } s.domStar = f[2] == "*" s.dowStar = f[4] == "*" return s, nil } func parseField(field string, lo, hi int, names map[string]int) ([64]bool, error) { var set [64]bool atom := func(s string) (int, error) { if names != nil { if v, ok := names[strings.ToLower(s)]; ok { return v, nil } } v, err := strconv.Atoi(s) if err != nil { return 0, fmt.Errorf("bad value %q", s) } return v, nil } for _, part := range strings.Split(field, ",") { step := 1 if i := strings.Index(part, "/"); i >= 0 { v, err := strconv.Atoi(part[i+1:]) if err != nil || v <= 0 { return set, fmt.Errorf("bad step in %q", part) } step, part = v, part[:i] } a, b := lo, hi switch { case part == "*": case strings.Contains(part, "-"): x, y, _ := strings.Cut(part, "-") var err error if a, err = atom(x); err != nil { return set, err } if b, err = atom(y); err != nil { return set, err } default: v, err := atom(part) if err != nil { return set, err } a = v if step == 1 { b = v } } if a < lo || b > hi || a > b { return set, fmt.Errorf("out of range: %q", part) } for v := a; v <= b; v += step { set[v] = true } } return set, nil } func (s *schedule) dayMatches(t time.Time) bool { if !s.mon[int(t.Month())] { return false } dom, dow := s.dom[t.Day()], s.dow[int(t.Weekday())] switch { case s.domStar && s.dowStar: return true case s.domStar: return dow case s.dowStar: return dom default: // both restricted: cron runs when either matches return dom || dow } } // NextRun is the first time strictly after `from` that a cron expression fires. Exported so // anything that writes a cron line — the ZFS snapshot schedules — can say when it runs next // using the same evaluator that reads everyone else's. func NextRun(expr string, from time.Time) (time.Time, bool) { return nextRun(expr, from) } // ValidSchedule reports whether a cron expression parses. func ValidSchedule(expr string) error { _, err := parseSchedule(expr) return err } // nextRun finds the first time strictly after `from` that the schedule fires. func nextRun(expr string, from time.Time) (time.Time, bool) { s, err := parseSchedule(expr) if err != nil || s.reboot { return time.Time{}, false } t := from.Truncate(time.Minute).Add(time.Minute) day := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) for i := 0; i < 366*2; i++ { if s.dayMatches(day) { for h := 0; h < 24; h++ { if !s.hour[h] { continue } for mi := 0; mi < 60; mi++ { if !s.min[mi] { continue } c := time.Date(day.Year(), day.Month(), day.Day(), h, mi, 0, 0, day.Location()) if !c.Before(t) { return c, true } } } } day = day.AddDate(0, 0, 1) } return time.Time{}, false }