package queue import ( "fmt" "strconv" "strings" "time" ) // Allowed values for quiet_mode (public config value). const ( QuietModeDiscard = "discard" QuietModeToast = "toast" ) // clockTime is a time of day at minute resolution (local time, 24 h). type clockTime struct{ hour, minute int } // minutes returns minutes since midnight. func (c clockTime) minutes() int { return c.hour*60 + c.minute } // String renders the time as "HH:MM". func (c clockTime) String() string { return fmt.Sprintf("%02d:%02d", c.hour, c.minute) } // QuietHours is a quiet window in local time, for example "18:00-08:00" // (spanning midnight). The zero value (Start == End) is disabled; silence // around the clock is mute, not quiet hours. type QuietHours struct { Start, End clockTime } // String renders the window as "HH:MM-HH:MM" (zero value: ""). func (q QuietHours) String() string { if q.Start == q.End { return "" } return q.Start.String() + "-" + q.End.String() } // ParseQuietHours parses a quiet window "HH:MM-HH:MM" (local time, 24 h; // hours with one or two digits). Windows across midnight (Start > End) are // allowed; Start == End disables the window. The empty string is the // disabled window (no error). func ParseQuietHours(s string) (QuietHours, error) { if s == "" { return QuietHours{}, nil } startS, endS, ok := strings.Cut(s, "-") if !ok { return QuietHours{}, fmt.Errorf("quiet_hours: %q is not HH:MM-HH:MM", s) } start, err := parseClock(startS) if err != nil { return QuietHours{}, fmt.Errorf("quiet_hours: %v", err) } end, err := parseClock(endS) if err != nil { return QuietHours{}, fmt.Errorf("quiet_hours: %v", err) } return QuietHours{Start: start, End: end}, nil } // parseClock parses "H[H]:MM" into its parts. func parseClock(s string) (clockTime, error) { hourS, minuteS, ok := strings.Cut(s, ":") if !ok || len(hourS) < 1 || len(hourS) > 2 || len(minuteS) != 2 { return clockTime{}, fmt.Errorf("%q is not HH:MM", s) } hour, err := strconv.Atoi(hourS) if err != nil { return clockTime{}, fmt.Errorf("%q is not HH:MM", s) } minute, err := strconv.Atoi(minuteS) if err != nil { return clockTime{}, fmt.Errorf("%q is not HH:MM", s) } if hour < 0 || hour > 23 || minute < 0 || minute > 59 { return clockTime{}, fmt.Errorf("%q is not a valid time of day (24 h)", s) } return clockTime{hour: hour, minute: minute}, nil } // Active reports whether the local time t lies inside the window. Day // windows (Start < End) run from Start up to but excluding End; night // windows (Start > End) span midnight. func (q QuietHours) Active(t time.Time) bool { if q.Start == q.End { return false } m := t.Hour()*60 + t.Minute() s, e := q.Start.minutes(), q.End.minutes() if s < e { return m >= s && m < e } return m >= s || m < e } // Until returns the local end of the currently active window; the zero time // when no window is active. Basis for GET /status. func (q QuietHours) Until(t time.Time) time.Time { if !q.Active(t) { return time.Time{} } end := time.Date(t.Year(), t.Month(), t.Day(), q.End.hour, q.End.minute, 0, 0, t.Location()) if q.Start.minutes() > q.End.minutes() && t.Hour()*60+t.Minute() >= q.Start.minutes() { end = end.AddDate(0, 0, 1) // night window: the end is tomorrow } return end }