package config import ( "fmt" "strconv" "strings" "time" ) // Window is a parsed maintenance-window expression. Times are minutes since // midnight in the local timezone. When End < Start, the window wraps // midnight (e.g. 23:00-01:00 means 23:00 today through 01:00 tomorrow). // // The zero value (Start == End == 0) means "always allowed" — used for // the empty-string-meaning-no-window case. type Window struct { Start int // minutes since midnight, [0, 1440) End int // minutes since midnight, [0, 1440) // alwaysOpen distinguishes "no constraint" from "midnight to midnight" // (the literal 00:00-00:00 window, which is a degenerate same-instant // window). Set when ParseWindow is called with an empty string. alwaysOpen bool } // AlwaysOpen returns true if this window imposes no constraint (the empty // string was parsed). func (w Window) AlwaysOpen() bool { return w.alwaysOpen } // ParseWindow parses "HH:MM-HH:MM" into a Window. Empty input returns an // AlwaysOpen window (no constraint). Whitespace around the input is tolerated. func ParseWindow(s string) (Window, error) { s = strings.TrimSpace(s) if s == "" { return Window{alwaysOpen: true}, nil } parts := strings.SplitN(s, "-", 2) if len(parts) != 2 { return Window{}, fmt.Errorf("maintenance window %q: expected HH:MM-HH:MM", s) } start, err := parseHHMM(strings.TrimSpace(parts[0])) if err != nil { return Window{}, fmt.Errorf("maintenance window %q: start: %w", s, err) } end, err := parseHHMM(strings.TrimSpace(parts[1])) if err != nil { return Window{}, fmt.Errorf("maintenance window %q: end: %w", s, err) } return Window{Start: start, End: end}, nil } func parseHHMM(s string) (int, error) { parts := strings.SplitN(s, ":", 2) if len(parts) != 2 { return 0, fmt.Errorf("%q: expected HH:MM", s) } h, err := strconv.Atoi(parts[0]) if err != nil || h < 0 || h > 23 { return 0, fmt.Errorf("%q: invalid hour", s) } m, err := strconv.Atoi(parts[1]) if err != nil || m < 0 || m > 59 { return 0, fmt.Errorf("%q: invalid minute", s) } return h*60 + m, nil } // Contains reports whether the given local time falls inside this window. // AlwaysOpen windows return true for any time. func (w Window) Contains(t time.Time) bool { if w.alwaysOpen { return true } now := t.Hour()*60 + t.Minute() if w.Start == w.End { // Degenerate: zero-length window. Never matches. return false } if w.Start < w.End { // Same-day window: [Start, End) return now >= w.Start && now < w.End } // Wrapping window: [Start, 1440) ∪ [0, End) return now >= w.Start || now < w.End } // String renders the window in HH:MM-HH:MM form for display. AlwaysOpen // renders as "always". func (w Window) String() string { if w.alwaysOpen { return "always" } return fmt.Sprintf("%02d:%02d-%02d:%02d", w.Start/60, w.Start%60, w.End/60, w.End%60) }