109 lines
3.3 KiB
Go
109 lines
3.3 KiB
Go
package queue
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Erlaubte Werte für quiet_mode (öffentlicher Config-Wert).
|
||
|
|
const (
|
||
|
|
QuietModeDiscard = "discard"
|
||
|
|
QuietModeToast = "toast"
|
||
|
|
)
|
||
|
|
|
||
|
|
// clockTime ist eine Tageszeit in Minuten-Auflösung (lokale Zeit, 24 h).
|
||
|
|
type clockTime struct{ hour, minute int }
|
||
|
|
|
||
|
|
// minutes liefert Minuten seit Mitternacht.
|
||
|
|
func (c clockTime) minutes() int { return c.hour*60 + c.minute }
|
||
|
|
|
||
|
|
// String rendert die Uhrzeit als "HH:MM".
|
||
|
|
func (c clockTime) String() string { return fmt.Sprintf("%02d:%02d", c.hour, c.minute) }
|
||
|
|
|
||
|
|
// QuietHours ist ein Ruhezeit-Fenster in lokaler Zeit, z. B. "18:00-08:00"
|
||
|
|
// (überspannt Mitternacht). Der Nullwert (Start == End) ist deaktiviert —
|
||
|
|
// rund-um-die-Uhr-Stumm ist mute, nicht quiet_hours.
|
||
|
|
type QuietHours struct {
|
||
|
|
Start, End clockTime
|
||
|
|
}
|
||
|
|
|
||
|
|
// String rendert das Fenster als "HH:MM-HH:MM" (Nullwert: "").
|
||
|
|
func (q QuietHours) String() string {
|
||
|
|
if q.Start == q.End {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
return q.Start.String() + "-" + q.End.String()
|
||
|
|
}
|
||
|
|
|
||
|
|
// ParseQuietHours parst ein Ruhezeit-Fenster "HH:MM-HH:MM" (lokale Zeit,
|
||
|
|
// 24 h; Stunden ein- oder zweistellig). Fenster über Mitternacht
|
||
|
|
// (Start > End) sind erlaubt; Start == End deaktiviert das Fenster. Der
|
||
|
|
// Leerstring ist das deaktivierte Fenster (kein Fehler).
|
||
|
|
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 ist nicht 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 parst "H[H]:MM" in Uhrzeit-Bestandteile.
|
||
|
|
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 ist nicht HH:MM", s)
|
||
|
|
}
|
||
|
|
hour, err := strconv.Atoi(hourS)
|
||
|
|
if err != nil {
|
||
|
|
return clockTime{}, fmt.Errorf("%q ist nicht HH:MM", s)
|
||
|
|
}
|
||
|
|
minute, err := strconv.Atoi(minuteS)
|
||
|
|
if err != nil {
|
||
|
|
return clockTime{}, fmt.Errorf("%q ist nicht HH:MM", s)
|
||
|
|
}
|
||
|
|
if hour < 0 || hour > 23 || minute < 0 || minute > 59 {
|
||
|
|
return clockTime{}, fmt.Errorf("%q ist keine gültige Uhrzeit (24 h)", s)
|
||
|
|
}
|
||
|
|
return clockTime{hour: hour, minute: minute}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Active meldet, ob der lokale Zeitpunkt t im Ruhefenster liegt.
|
||
|
|
// Tagfenster (Start < End) gelten ab Start bis ausschließlich End;
|
||
|
|
// Nachtfenster (Start > End) überspannen Mitternacht.
|
||
|
|
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 liefert das lokale Ende des gerade aktiven Fensters — Nullzeit,
|
||
|
|
// wenn gerade kein Ruhefenster aktiv ist. Basis für 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) // Nachtfenster: das Ende ist am Folgetag
|
||
|
|
}
|
||
|
|
return end
|
||
|
|
}
|