package datetime import ( "fmt" "log/slog" "strconv" "strings" "time" "github.com/egoist/mygo/ui" "git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core" "git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/input" "git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme" ) // CronSchedule is a parsed five-field cron expression. type CronSchedule struct { fields [5]uint64 tokens [5]string // domAny and dowAny note a "*" day field. domAny, dowAny bool } // CronProblem is what is wrong with a cron field. type CronProblem int const ( CronFieldCount CronProblem = iota CronEmptyItem CronBadStep CronNotNumber CronOutOfRange CronBackwards ) // CronError locates a problem in a cron expression. type CronError struct { // Field is 0 to 4, or -1 for the count. Field int Problem CronProblem // Text is the offending part. Text string } var cronProblemKeys = [...]string{"cron_editor.err_count", "cron_editor.err_empty", "cron_editor.err_step", "cron_editor.err_number", "cron_editor.err_range", "cron_editor.err_backwards"} func (e *CronError) Error() string { return fmt.Sprintf("cron field %d: problem %d at %q", e.Field+1, e.Problem, e.Text) } var cronBounds = [5][2]int{{0, 59}, {0, 23}, {1, 31}, {1, 12}, {0, 7}} var cronNames = [5][]string{3: {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}, 4: {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}} // ParseCron parses standard five-field cron with names. func ParseCron(expr string) (CronSchedule, error) { parts := strings.Fields(expr) var s CronSchedule if len(parts) != 5 { return s, &CronError{-1, CronFieldCount, strconv.Itoa(len(parts))} } for f, part := range parts { s.tokens[f] = part for _, item := range strings.Split(part, ",") { bits, err := cronItem(f, strings.ToUpper(item)) if err != nil { err.Field = f return CronSchedule{}, err } s.fields[f] |= bits } } if s.fields[4]&(1<<7) != 0 { s.fields[4] |= 1 } s.domAny, s.dowAny = parts[2] == "*", parts[4] == "*" return s, nil } // cronItem parses one list item into a bit set. func cronItem(f int, item string) (uint64, *CronError) { lo, hi := cronBounds[f][0], cronBounds[f][1] if item == "" { return 0, &CronError{Problem: CronEmptyItem} } rng, step := item, 1 if i := strings.IndexByte(item, '/'); i >= 0 { n, err := strconv.Atoi(item[i+1:]) if err != nil || n < 1 { return 0, &CronError{Problem: CronBadStep, Text: item[i+1:]} } rng, step = item[:i], n } a, b := lo, hi switch { case rng == "*": case strings.Contains(rng, "-"): x, y, _ := strings.Cut(rng, "-") var e *CronError if a, e = cronValue(f, x); e != nil { return 0, e } if b, e = cronValue(f, y); e != nil { return 0, e } if a > b { return 0, &CronError{Problem: CronBackwards, Text: rng} } default: var e *CronError if a, e = cronValue(f, rng); e != nil { return 0, e } if step == 1 { b = a } } var bits uint64 for v := a; v <= b; v += step { bits |= 1 << v } return bits, nil } func cronValue(f int, s string) (int, *CronError) { for i, n := range cronNames[f] { if s == n { return i + cronBounds[f][0], nil } } v, err := strconv.Atoi(s) if err != nil { return 0, &CronError{Problem: CronNotNumber, Text: s} } if v < cronBounds[f][0] || v > cronBounds[f][1] { return 0, &CronError{Problem: CronOutOfRange, Text: s} } return v, nil } // CronRun is one upcoming run; Repeated marks a DST repeat. type CronRun struct { At time.Time Repeated bool } // Next returns up to n runs within five years. func (s CronSchedule) Next(t time.Time, loc *time.Location, n int) []CronRun { var out []CronRun w := t.In(loc) day := core.DateOf(w) for i := 0; i < 5*366 && len(out) < n; i, day = i+1, day.AddDays(1) { if !s.dayMatches(day) { continue } for h := 0; h < 24 && len(out) < n; h++ { if s.fields[1]&(1< 1}) } } } return out } // dayMatches applies cron's either-day-field rule. func (s CronSchedule) dayMatches(d core.Date) bool { if s.fields[3]&(1<