Files
talk/internal/queue/quiet.go
T

109 lines
3.2 KiB
Go
Raw Permalink Normal View History

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
}