Öffnungszeiten: Live-Status, Texte und Wechselzeitpunkte

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 23:59:20 +00:00
1 parent 0f25f7cb52
commit 59b0ea799d
2 files changed
+604 -2

No files matched your search

+181 -1
View File
@@ -1,4 +1,4 @@
import { zuUtc, wochentag, plusTage } from '../termine/zeit.js'
import { zuUtc, ausUtc, wochentag, plusTage } from '../termine/zeit.js'
import { BUNDESLAENDER, feiertageImZeitraum } from '../termine/feiertage.js'
/**
@@ -234,3 +234,183 @@ export function oeffnungsIntervalle(daten, plan, vonTag, bisTag) {
}
return zusammen
}
// ------------------------------------------------------------------ Texte, Status und Wechsel
const WOCHENTAGE = Object.freeze(['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'])
const MINUTE_MS = 60 * 1000
const STUNDE_MS = 60 * MINUTE_MS
const SUCH_TAGE = 60 // Spec 6: keine Oeffnung in 60 Tagen => „Derzeit geschlossen“
const RUND_TAGE = 7 // durchgehend mindestens 7 Tage => „Rund um die Uhr geoeffnet“
const WECHSEL_TAGE = 8 // Spec 4.1: wechsel deckt 8 Tage ab
const WOCHEN_TAGE = 6 // bis hierhin nennt der Text den Wochentag, danach das Datum
const MAX_TAGE_LISTE = 366
const KURZ_OFFEN = 'Geöffnet'
const KURZ_ZU = 'Geschlossen'
const KURZ_VEREINBARUNG = 'Nach Vereinbarung'
function pruefeJetzt(now) {
if (!(now instanceof Date) || !Number.isFinite(now.getTime())) throw new RangeError('now: ungueltiger Zeitpunkt')
return now.getTime()
}
/**
* Uhrzeit im Fliesstext: '09:00' => '9 Uhr', '18:30' => '18:30 Uhr', '09:05' => '9:05 Uhr';
* '00:00' und '24:00' => 'Mitternacht' (nie „24 Uhr“). Ungueltige Eingabe wirft RangeError.
*/
export function uhrText(hhmm) {
const min = minuten(hhmm)
if (min === null) throw new RangeError(`uhrText: ungueltige Uhrzeit ${String(hhmm)}`)
if (min === 0 || min === 1440) return 'Mitternacht'
const h = Math.floor(min / 60)
const m = min % 60
return m === 0 ? `${h} Uhr` : `${h}:${pad(m)} Uhr`
}
/** 'YYYY-MM-DD' -> 'TT.MM.' */
const datumText = (tag) => `${tag.slice(8, 10)}.${tag.slice(5, 7)}.`
const wochentagsName = (tag) => WOCHENTAGE[wochentag(tag) - 1]
/** Kalendertage von bis nach (0..max) oder null, wenn weiter weg (nur ueber plusTage gezaehlt). */
function tagAbstand(von, bis, max) {
let tag = von
for (let n = 0; n <= max; n += 1) {
if (tag === bis) return n
tag = plusTage(tag, 1)
}
return null
}
/** Letzter Tag der zusammenhaengenden Urlaubstage ab tag (tag selbst ist Urlaub). */
function urlaubBis(daten, plan, tag) {
let letzter = tag
for (let i = 0; i < 400; i += 1) {
const naechster = plusTage(letzter, 1)
if (tagesbild(daten, plan, naechster).art !== 'urlaub') break
letzter = naechster
}
return letzter
}
/** Text fuer ein laufendes Intervall, das um bisMs endet (jetzt = Berliner Wanduhr von nowMs). */
function offenText(bisMs, nowMs, jetzt) {
const siebenTage = zuUtcOderDanach(plusTage(jetzt.tag, RUND_TAGE), jetzt.uhrzeit).getTime()
if (bisMs >= siebenTage) return 'Rund um die Uhr geöffnet'
const ende = ausUtc(new Date(bisMs))
if (bisMs - nowMs <= 24 * STUNDE_MS) return `Jetzt geöffnet · schließt um ${uhrText(ende.uhrzeit)}`
// Schluss genau um Mitternacht: der Tag, der endet („bis Dienstag Mitternacht“), nicht der Folgetag
const tag = ende.uhrzeit === '00:00' ? plusTage(ende.tag, -1) : ende.tag
return tagAbstand(jetzt.tag, tag, WOCHEN_TAGE) === null
? `Jetzt geöffnet · bis ${datumText(tag)} um ${uhrText(ende.uhrzeit)}`
: `Jetzt geöffnet · bis ${wochentagsName(tag)} ${uhrText(ende.uhrzeit)}`
}
/** Text fuer die naechste Oeffnung (vonMs) von heute (Berliner Kalendertag) aus gesehen. */
function oeffnetText(vonMs, heute) {
const start = ausUtc(new Date(vonMs))
// Beginn um 00:00 heisst „0 Uhr“: „um Mitternacht“ wuerde als Tagesende gelesen
const zeit = start.uhrzeit === '00:00' ? '0 Uhr' : uhrText(start.uhrzeit)
const abstand = tagAbstand(heute, start.tag, WOCHEN_TAGE)
if (abstand === 0) return `Geschlossen · öffnet heute um ${zeit}`
if (abstand === 1) return `Geschlossen · öffnet morgen um ${zeit}`
if (abstand === null) return `Geschlossen · öffnet am ${datumText(start.tag)} um ${zeit}`
return `Geschlossen · öffnet ${wochentagsName(start.tag)} um ${zeit}`
}
/**
* Status zum Zeitpunkt nowMs auf Basis fertiger Intervalle (muessen den Tag des Zeitpunkts minus 1 bis
* mindestens 60 Tage danach abdecken, siehe oeffnungsIntervalle).
*/
function statusAus(daten, plan, nowMs, intervalle) {
const jetzt = ausUtc(new Date(nowMs))
const heute = jetzt.tag
const laufend = intervalle.find((i) => i.vonMs <= nowMs && nowMs < i.bisMs)
if (laufend) return { offen: true, text: offenText(laufend.bisMs, nowMs, jetzt), kurz: KURZ_OFFEN }
const bild = tagesbild(daten, plan, heute)
if (bild.modus === 'vereinbarung') return { offen: null, text: 'Heute nach Vereinbarung', kurz: KURZ_VEREINBARUNG }
if (bild.modus === 'zu' && bild.art === 'urlaub') {
const bis = datumText(urlaubBis(daten, plan, heute))
return { offen: false, text: `Heute geschlossen (Betriebsurlaub bis ${bis})`, kurz: KURZ_ZU }
}
if (bild.modus === 'zu' && bild.art === 'feiertag') {
const grund = bild.name ? `Feiertag: ${bild.name}` : 'Feiertag'
return { offen: false, text: `Heute geschlossen (${grund})`, kurz: KURZ_ZU }
}
const grenze = plusTage(heute, SUCH_TAGE)
const naechste = intervalle.find((i) => i.vonMs > nowMs)
if (naechste && ausUtc(new Date(naechste.vonMs)).tag <= grenze) {
return { offen: false, text: oeffnetText(naechste.vonMs, heute), kurz: KURZ_ZU }
}
return { offen: false, text: 'Derzeit geschlossen', kurz: KURZ_ZU }
}
/**
* Status eines Plans zum Zeitpunkt now (Date, immer Parameter): { offen: true|false|null, text, kurz }.
* Texte nach Spec 6 (ohne Anrede, nie „24 Uhr“); offen null = nach Vereinbarung. Wirft RangeError bei
* ungueltigem now.
*/
export function statusZu(daten, plan, now) {
const nowMs = pruefeJetzt(now)
const heute = ausUtc(now).tag
// 60 Tage Vorschau fuer die naechste Oeffnung; ein laenger laufendes Intervall ist ohnehin >= 7 Tage
return statusAus(daten, plan, nowMs, oeffnungsIntervalle(daten, plan, heute, plusTage(heute, SUCH_TAGE)))
}
/**
* Alle Statuswechsel der naechsten 8 Tage: [{ ab (UTC-ms), tag (Berliner Datum bei ab), offen, text, kurz }].
* Der erste Eintrag gilt ab Beginn der aktuellen Minute; danach ein Eintrag je Aenderung von offen/text/kurz
* an einer Intervallgrenze (Oeffnen/Schliessen) und an jeder Berliner Mitternacht (z. B. „morgen“ -> „heute“).
* Nicht enthalten sind Textwechsel mitten am Tag, die nur die Restdauer betreffen (bis Dienstag -> schliesst um).
*/
export function wechselListe(daten, plan, now) {
const startMs = Math.floor(pruefeJetzt(now) / MINUTE_MS) * MINUTE_MS
const start = ausUtc(new Date(startMs))
const endeMs = zuUtcOderDanach(plusTage(start.tag, WECHSEL_TAGE), start.uhrzeit).getTime()
// 8 Tage Wechsel + 60 Tage Vorschau fuer den letzten Eintrag
const intervalle = oeffnungsIntervalle(daten, plan, start.tag, plusTage(start.tag, WECHSEL_TAGE + SUCH_TAGE))
const kandidaten = new Set([startMs])
const vormerken = (ms) => {
if (ms > startMs && ms < endeMs) kandidaten.add(ms)
}
for (const i of intervalle) {
vormerken(i.vonMs)
vormerken(i.bisMs)
}
for (let n = 1; n <= WECHSEL_TAGE; n += 1) vormerken(zuUtcOderDanach(plusTage(start.tag, n), '00:00').getTime())
const liste = []
let vorher = null
for (const ms of [...kandidaten].sort((a, b) => a - b)) {
const s = statusAus(daten, plan, ms, intervalle)
if (vorher && vorher.offen === s.offen && vorher.text === s.text && vorher.kurz === s.kurz) continue
vorher = s
liste.push({ ab: ms, tag: ausUtc(new Date(ms)).tag, offen: s.offen, text: s.text, kurz: s.kurz })
}
return liste
}
/**
* Tagesbilder fuer die API: anzahl aufeinanderfolgende Tage ab vonTag als
* [{ datum, art, name, modus, fenster }] (wie tagesbild, nur heisst der Tag „datum“).
* Ungueltiger Tag oder anzahl ausserhalb 0..366 wirft RangeError.
*/
export function tageListe(daten, plan, vonTag, anzahl) {
pruefeTag(vonTag, 'vonTag')
if (!Number.isInteger(anzahl) || anzahl < 0 || anzahl > MAX_TAGE_LISTE) {
throw new RangeError(`anzahl: ungueltig ${String(anzahl)}`)
}
const liste = []
let tag = vonTag
for (let i = 0; i < anzahl; i += 1) {
const b = tagesbild(daten, plan, tag)
liste.push({ datum: b.tag, art: b.art, name: b.name, modus: b.modus, fenster: b.fenster })
tag = plusTage(tag, 1)
}
return liste
}