// Package engine provides the automation rule engine and reminder scheduler. // Rules fire actions through the AI pipeline (which can call tools like IoT control). package engine import ( "fmt" "strconv" "strings" "time" ) // cronField matches one field of a cron expression. type cronField struct{ values map[int]bool } // parseCronField parses "*", "*/n", "a", "a,b,c", "a-b", "a-b/n". func parseCronField(s string, min, max int) (cronField, error) { f := cronField{values: map[int]bool{}} for _, part := range strings.Split(s, ",") { part = strings.TrimSpace(part) if part == "" { return f, fmt.Errorf("empty cron field part") } step := 1 base := part if i := strings.Index(part, "/"); i >= 0 { base = part[:i] n, err := strconv.Atoi(part[i+1:]) if err != nil || n <= 0 { return f, fmt.Errorf("invalid step %q", part) } step = n } lo, hi := min, max switch { case base == "*": case strings.Contains(base, "-"): segs := strings.SplitN(base, "-", 2) a, err1 := strconv.Atoi(segs[0]) b, err2 := strconv.Atoi(segs[1]) if err1 != nil || err2 != nil { return f, fmt.Errorf("invalid range %q", base) } lo, hi = a, b default: n, err := strconv.Atoi(base) if err != nil { return f, fmt.Errorf("invalid value %q", base) } lo, hi = n, n } if lo < min || hi > max || lo > hi { return f, fmt.Errorf("value out of range %q", base) } for v := lo; v <= hi; v += step { f.values[v] = true } } return f, nil } // CronSchedule is a parsed 5-field cron expression (min hour dom month dow). type CronSchedule struct { min, hour, dom, month, dow cronField } // ParseCron parses a standard 5-field cron expression. func ParseCron(expr string) (*CronSchedule, error) { parts := strings.Fields(expr) if len(parts) != 5 { return nil, fmt.Errorf("cron must have 5 fields, got %d", len(parts)) } bounds := [5][2]int{{0, 59}, {0, 23}, {1, 31}, {1, 12}, {0, 6}} var fields [5]cronField for i, p := range parts { f, err := parseCronField(p, bounds[i][0], bounds[i][1]) if err != nil { return nil, fmt.Errorf("field %d: %w", i+1, err) } fields[i] = f } return &CronSchedule{fields[0], fields[1], fields[2], fields[3], fields[4]}, nil } // Match reports whether t matches the schedule (minute precision). func (c *CronSchedule) Match(t time.Time) bool { return c.min.values[t.Minute()] && c.hour.values[t.Hour()] && c.dom.values[t.Day()] && c.month.values[int(t.Month())] && c.dow.values[int(t.Weekday())] }