Files
Webklar-Kundenbereich/test/zeitenZeitlogik.test.js
T
ClaudeandClaude Opus 5.5 61c2f72631 Öffnungszeiten: Hinweise, JSON-LD, Google-Text und Erinnerungstage
Reine Funktionen in zeitlogik.js: hinweisZeitraum/gueltigeHinweise (Berliner Kalendertage,
Rang, Beginn in 8 Tagen), jsonLdFuer (Mitternachtsfenster, rund 00:00–23:59, Sonderzeiten
der nächsten 60 Tage, neuerBlock mit @context), googleText, erinnerungsTage und
imErinnerungsfenster (08:00–20:00 Berlin).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:12:20 +00:00

1452 lines
80 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import {
hauptplan,
planNachKurz,
zuUtcOderDanach,
tagesbild,
oeffnungsIntervalle,
uhrText,
statusZu,
wechselListe,
tageListe,
HINWEIS_RANG,
hinweisZeitraum,
gueltigeHinweise,
jsonLdFuer,
googleText,
erinnerungsTage,
imErinnerungsfenster,
} from '../server/services/zeiten/zeitlogik.js'
import { ausUtc, zuUtc } from '../server/services/termine/zeit.js'
// Fixture: Wochentage 1..7 (Mo..So). Wert: ['09:00','18:00'] = ein Fenster, [[..],[..]] = zwei Fenster,
// 'rund' / 'vereinbarung' = Modus, fehlt = zu.
function plan(woche = {}, extra = {}) {
const tage = {}
for (let d = 1; d <= 7; d += 1) {
const w = woche[d]
if (!w) tage[d] = { modus: 'zu', fenster: [] }
else if (typeof w === 'string') tage[d] = { modus: w, fenster: [] }
else tage[d] = { modus: 'zeiten', fenster: Array.isArray(w[0]) ? w : [w] }
}
return { id: 'p1', kurz: 'standard', name: 'Standard', haupt: true, tage, ...extra }
}
function daten({ land = 'RP', plaene = [plan({ 1: ['09:00', '18:00'] })], ausnahmen = [], feiertage = {}, hinweise = [] } = {}) {
return {
land,
plaene,
feiertage,
ausnahmen,
hinweise,
kontakt: { an: false, telefon: '', adresse: '', whatsapp: null },
}
}
function tiefEinfrieren(x) {
if (x && typeof x === 'object' && !Object.isFrozen(x)) {
Object.freeze(x)
for (const v of Object.values(x)) tiefEinfrieren(v)
}
return x
}
const urlaub = (extra) => ({ id: 'u', art: 'urlaub', text: 'Betriebsurlaub', modus: 'zu', fenster: [], plan: null, ...extra })
const sondertag = (extra) => ({ id: 's', art: 'sondertag', text: 'Sonderöffnung', modus: 'zeiten', fenster: [['10:00', '14:00']], plan: null, ...extra })
// Berliner Wanduhr eines Zeitpunkts in ms: '2026-10-10 18:00'
const berlin = (ms) => {
const { tag, uhrzeit } = ausUtc(new Date(ms))
return `${tag} ${uhrzeit}`
}
const iv = (liste) => liste.map((i) => [berlin(i.vonMs), berlin(i.bisMs)])
// ---------------------------------------------------------------- Plan-Suche
test('hauptplan liefert den Plan mit haupt: true, sonst null', () => {
const a = plan({}, { id: 'a', kurz: 'sommer', haupt: false })
const b = plan({}, { id: 'b', kurz: 'standard', haupt: true })
assert.equal(hauptplan(daten({ plaene: [a, b] })), b)
assert.equal(hauptplan(daten({ plaene: [a] })), null)
assert.equal(hauptplan(daten({ plaene: [] })), null)
assert.equal(hauptplan({}), null)
assert.equal(hauptplan(null), null)
assert.equal(hauptplan(undefined), null)
assert.equal(hauptplan({ plaene: 'kaputt' }), null)
})
test('planNachKurz findet den Plan über den Kurznamen', () => {
const a = plan({}, { id: 'a', kurz: 'sommer', haupt: false })
const b = plan({}, { id: 'b', kurz: 'standard', haupt: true })
const d = daten({ plaene: [a, b] })
assert.equal(planNachKurz(d, 'sommer'), a)
assert.equal(planNachKurz(d, 'standard'), b)
assert.equal(planNachKurz(d, 'winter'), null)
assert.equal(planNachKurz(d, 'SOMMER'), null, 'Kurznamen sind [a-z0-9-], kein Case-Folding')
assert.equal(planNachKurz(d, undefined), null)
assert.equal(planNachKurz(d, 5), null)
assert.equal(planNachKurz(null, 'sommer'), null)
})
// ---------------------------------------------------------------- zuUtcOderDanach
test('März-Lücke rückt auf 03:00', () => {
assert.equal(zuUtcOderDanach('2026-03-29', '02:30').toISOString(), '2026-03-29T01:00:00.000Z')
assert.equal(zuUtcOderDanach('2026-03-29', '02:00').toISOString(), '2026-03-29T01:00:00.000Z')
assert.equal(zuUtcOderDanach('2026-03-29', '02:59').toISOString(), '2026-03-29T01:00:00.000Z')
assert.equal(zuUtcOderDanach('2026-03-29', '03:00').toISOString(), '2026-03-29T01:00:00.000Z')
assert.equal(zuUtcOderDanach('2026-03-29', '01:59').toISOString(), '2026-03-29T00:59:00.000Z')
})
test('Oktober: doppelte Stunde nimmt das erste Vorkommen', () => {
assert.equal(zuUtcOderDanach('2026-10-25', '02:30').toISOString(), '2026-10-25T00:30:00.000Z')
assert.equal(zuUtcOderDanach('2026-10-25', '03:00').toISOString(), '2026-10-25T02:00:00.000Z')
})
test('zuUtcOderDanach: normale Tage wie zuUtc, 24:00 ist der Folgetag 00:00', () => {
assert.equal(zuUtcOderDanach('2026-10-05', '09:00').toISOString(), '2026-10-05T07:00:00.000Z')
assert.equal(zuUtcOderDanach('2026-01-15', '09:00').toISOString(), '2026-01-15T08:00:00.000Z')
assert.equal(zuUtcOderDanach('2026-10-05', '24:00').toISOString(), '2026-10-05T22:00:00.000Z')
assert.equal(zuUtcOderDanach('2026-10-24', '24:00').toISOString(), '2026-10-24T22:00:00.000Z', 'Mitternacht vor der Umstellung: Sommerzeit')
assert.equal(zuUtcOderDanach('2026-10-25', '24:00').toISOString(), '2026-10-25T23:00:00.000Z', 'Mitternacht nach der Umstellung: Winterzeit')
assert.equal(zuUtcOderDanach('2026-12-31', '24:00').toISOString(), '2026-12-31T23:00:00.000Z')
assert.equal(zuUtcOderDanach('2026-03-28', '24:00').toISOString(), '2026-03-28T23:00:00.000Z')
})
test('zuUtcOderDanach wirft bei ungültiger Eingabe (nur die Lücke wird überbrückt)', () => {
assert.throws(() => zuUtcOderDanach('2026-02-30', '09:00'), RangeError)
assert.throws(() => zuUtcOderDanach('2026-10-05', '25:00'), RangeError)
assert.throws(() => zuUtcOderDanach('2026-10-05', '9:00'), RangeError)
assert.throws(() => zuUtcOderDanach('2026-10-05', '24:30'), RangeError)
assert.throws(() => zuUtcOderDanach(undefined, '09:00'), RangeError)
assert.throws(() => zuUtcOderDanach('2026-10-05'), RangeError)
})
// ---------------------------------------------------------------- tagesbild: Vorrang
test('Wochenplan an einem normalen Montag', () => {
const d = daten()
assert.deepEqual(tagesbild(d, hauptplan(d), '2026-10-05'), {
tag: '2026-10-05',
art: 'regulaer',
name: null,
modus: 'zeiten',
fenster: [['09:00', '18:00']],
})
})
test('Wochenplan: zwei Fenster, geschlossene Tage, rund und nach Vereinbarung', () => {
const p = plan({ 1: [['09:00', '12:30'], ['13:30', '18:00']], 3: 'rund', 4: 'vereinbarung' })
const d = daten({ plaene: [p] })
assert.deepEqual(tagesbild(d, p, '2026-10-05').fenster, [['09:00', '12:30'], ['13:30', '18:00']])
assert.deepEqual(tagesbild(d, p, '2026-10-06'), { tag: '2026-10-06', art: 'regulaer', name: null, modus: 'zu', fenster: [] })
assert.deepEqual(tagesbild(d, p, '2026-10-07'), { tag: '2026-10-07', art: 'regulaer', name: null, modus: 'rund', fenster: [] })
assert.deepEqual(tagesbild(d, p, '2026-10-08'), { tag: '2026-10-08', art: 'regulaer', name: null, modus: 'vereinbarung', fenster: [] })
assert.equal(tagesbild(d, p, '2026-10-11').modus, 'zu', 'Sonntag ohne Eintrag')
})
test('fehlende oder kaputte Tageseinträge ergeben „zu“, kaputte Fenster fallen heraus', () => {
const p = { id: 'p1', kurz: 'standard', name: 'S', haupt: true, tage: { 1: { modus: 'zeiten', fenster: [['9:00', '18:00'], ['09:00'], 'x', ['10:00', '10:00'], ['24:00', '02:00'], ['09:00', '25:00']] }, 2: { modus: 'unbekannt', fenster: [['09:00', '18:00']] }, 3: { modus: 'zeiten', fenster: [['09:00', '12:00'], ['kaputt', '18:00']] } } }
const d = daten({ plaene: [p] })
assert.deepEqual(tagesbild(d, p, '2026-10-05'), { tag: '2026-10-05', art: 'regulaer', name: null, modus: 'zu', fenster: [] }, 'Modus zeiten ohne brauchbares Fenster = zu')
assert.equal(tagesbild(d, p, '2026-10-06').modus, 'zu', 'unbekannter Modus')
assert.deepEqual(tagesbild(d, p, '2026-10-07').fenster, [['09:00', '12:00']])
assert.equal(tagesbild(d, p, '2026-10-08').modus, 'zu', 'Tag fehlt')
assert.equal(tagesbild(d, { id: 'x', tage: null }, '2026-10-05').modus, 'zu')
assert.equal(tagesbild(d, null, '2026-10-05').modus, 'zu', 'ohne Plan nur Ausnahmen')
})
test('Fenster werden nach Beginn sortiert und kopiert', () => {
const p = plan({ 1: [['13:30', '18:00'], ['09:00', '12:30']] })
const d = daten({ plaene: [p] })
const t = tagesbild(d, p, '2026-10-05')
assert.deepEqual(t.fenster, [['09:00', '12:30'], ['13:30', '18:00']])
t.fenster[0][0] = '00:00'
t.fenster.push(['20:00', '21:00'])
assert.deepEqual(tagesbild(d, p, '2026-10-05').fenster, [['09:00', '12:30'], ['13:30', '18:00']], 'Rückgabe teilt keine Objekte mit den Daten')
assert.deepEqual(p.tage[1].fenster, [['13:30', '18:00'], ['09:00', '12:30']], 'Eingabe bleibt unverändert')
})
test('Feiertag ist zu, mit Namen', () => {
const d = daten()
assert.deepEqual(tagesbild(d, hauptplan(d), '2026-10-03'), {
tag: '2026-10-03',
art: 'feiertag',
name: 'Tag der Deutschen Einheit',
modus: 'zu',
fenster: [],
})
})
test('Feiertag gilt auch an einem Tag, an dem der Plan offen wäre', () => {
const p = plan({ 6: ['10:00', '16:00'] })
const d = daten({ plaene: [p] })
const t = tagesbild(d, p, '2026-10-03') // Samstag
assert.equal(t.art, 'feiertag')
assert.equal(t.modus, 'zu')
})
test('Feiertag hängt vom Bundesland ab; fehlendes oder ungültiges Land = RP', () => {
const p = plan({ 1: ['09:00', '18:00'] })
// Fronleichnam 2026 = 04.06. (Donnerstag): in RP gesetzlich, in BE nicht
const ohneLand = daten({ plaene: [p] })
delete ohneLand.land
assert.equal(tagesbild(daten({ land: 'RP', plaene: [p] }), p, '2026-06-04').art, 'feiertag')
assert.equal(tagesbild(daten({ land: 'BE', plaene: [p] }), p, '2026-06-04').art, 'regulaer')
assert.equal(tagesbild(ohneLand, p, '2026-06-04').art, 'feiertag', 'Vorgabe RP')
assert.equal(tagesbild(daten({ land: 'XX', plaene: [p] }), p, '2026-06-04').art, 'feiertag', 'ungültiges Land: Vorgabe RP')
assert.equal(tagesbild(daten({ land: 'rp', plaene: [p] }), p, '2026-06-04').art, 'feiertag', 'Kleinschreibung: Vorgabe RP')
assert.equal(tagesbild({ plaene: [p] }, p, '2026-10-03').name, 'Tag der Deutschen Einheit', 'daten ohne Land, Ausnahmen und Feiertage')
})
test('Feiertags-Abweichung gilt', () => {
const d = daten({ feiertage: { '2026-10-03': { modus: 'zeiten', fenster: [['10:00', '14:00']] } } })
assert.deepEqual(tagesbild(d, hauptplan(d), '2026-10-03'), {
tag: '2026-10-03',
art: 'feiertag',
name: 'Tag der Deutschen Einheit',
modus: 'zeiten',
fenster: [['10:00', '14:00']],
})
const rund = daten({ feiertage: { '2026-10-03': { modus: 'rund', fenster: [] } } })
assert.equal(tagesbild(rund, hauptplan(rund), '2026-10-03').modus, 'rund')
const verein = daten({ feiertage: { '2026-10-03': { modus: 'vereinbarung', fenster: [] } } })
assert.equal(tagesbild(verein, hauptplan(verein), '2026-10-03').modus, 'vereinbarung')
})
test('Feiertags-Abweichung auf einem Tag ohne Feiertag oder mit kaputtem Inhalt wirkt nicht', () => {
const d = daten({ feiertage: { '2026-10-05': { modus: 'zu', fenster: [] }, '2026-10-03': { modus: 'kaputt' } } })
const p = hauptplan(d)
assert.equal(tagesbild(d, p, '2026-10-05').modus, 'zeiten', 'kein Feiertag: Wochenplan')
assert.equal(tagesbild(d, p, '2026-10-03').modus, 'zu', 'kaputte Abweichung: Vorgabe des Feiertags')
assert.equal(tagesbild(d, p, '2026-10-03').art, 'feiertag')
})
test('Prüftag folgt dem Wochenplan', () => {
const p = plan({ 4: ['09:00', '16:00'] })
const d = daten({ plaene: [p] })
assert.deepEqual(tagesbild(d, p, '2026-12-24'), {
tag: '2026-12-24',
art: 'regulaer',
name: null,
modus: 'zeiten',
fenster: [['09:00', '16:00']],
})
// Silvester 2026 (Do) ebenso; 1. Weihnachtstag 2026 (Fr) ist gesetzlich
assert.equal(tagesbild(d, p, '2026-12-31').art, 'regulaer')
assert.equal(tagesbild(d, p, '2026-12-25').art, 'feiertag')
// Mariä Himmelfahrt: in BY ein Prüftag, in SL gesetzlich
const by = daten({ land: 'BY', plaene: [plan({ 6: ['09:00', '12:00'] })] })
assert.equal(tagesbild(by, by.plaene[0], '2026-08-15').art, 'regulaer')
const sl = daten({ land: 'SL', plaene: [plan({ 6: ['09:00', '12:00'] })] })
assert.equal(tagesbild(sl, sl.plaene[0], '2026-08-15').art, 'feiertag')
})
test('Prüftag mit gespeicherter Abweichung: die Abweichung gilt, der Name bleibt', () => {
const p = plan({ 4: ['09:00', '18:00'] })
const d = daten({ plaene: [p], feiertage: { '2026-12-24': { modus: 'zeiten', fenster: [['09:00', '13:00']] } } })
assert.deepEqual(tagesbild(d, p, '2026-12-24'), {
tag: '2026-12-24',
art: 'feiertag',
name: 'Heiligabend',
modus: 'zeiten',
fenster: [['09:00', '13:00']],
})
const zu = daten({ plaene: [p], feiertage: { '2026-12-24': { modus: 'zu', fenster: [] } } })
assert.equal(tagesbild(zu, p, '2026-12-24').modus, 'zu')
})
test('Urlaub schlägt Feiertag, Sondertag schlägt Urlaub', () => {
const p = plan({ 1: ['09:00', '18:00'], 6: ['10:00', '16:00'] })
// Feiertag Sa 03.10.: Urlaub zu; Sondertag offen
const nurUrlaub = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-01', bis: '2026-10-06', text: 'Herbstpause' })] })
assert.deepEqual(tagesbild(nurUrlaub, p, '2026-10-03'), {
tag: '2026-10-03',
art: 'urlaub',
name: 'Herbstpause',
modus: 'zu',
fenster: [],
})
const urlaubUndFeiertagsAbweichung = daten({
plaene: [p],
ausnahmen: [urlaub({ von: '2026-10-01', bis: '2026-10-06' })],
feiertage: { '2026-10-03': { modus: 'zeiten', fenster: [['10:00', '14:00']] } },
})
assert.equal(tagesbild(urlaubUndFeiertagsAbweichung, p, '2026-10-03').art, 'urlaub', 'Urlaub schlägt auch die Feiertags-Abweichung')
const mitSondertag = daten({
plaene: [p],
ausnahmen: [urlaub({ von: '2026-10-01', bis: '2026-10-06' }), sondertag({ von: '2026-10-03', bis: '2026-10-03', text: 'Tag der offenen Tür' })],
})
assert.deepEqual(tagesbild(mitSondertag, p, '2026-10-03'), {
tag: '2026-10-03',
art: 'sondertag',
name: 'Tag der offenen Tür',
modus: 'zeiten',
fenster: [['10:00', '14:00']],
})
assert.equal(tagesbild(mitSondertag, p, '2026-10-05').art, 'urlaub', 'Montag im Urlaub, kein Sondertag')
assert.equal(tagesbild(mitSondertag, p, '2026-10-07').art, 'regulaer', 'nach dem Urlaub')
assert.equal(tagesbild(mitSondertag, p, '2026-09-30').art, 'regulaer', 'vor dem Urlaub')
})
test('Urlaub ist immer zu, auch wenn der gespeicherte Modus etwas anderes sagt', () => {
const p = plan({ 1: ['09:00', '18:00'] })
const d = daten({ plaene: [p], ausnahmen: [{ id: 'u', art: 'urlaub', von: '2026-10-05', bis: '2026-10-05', plan: null }] })
const t = tagesbild(d, p, '2026-10-05')
assert.equal(t.art, 'urlaub')
assert.equal(t.modus, 'zu')
assert.deepEqual(t.fenster, [])
assert.equal(t.name, null, 'ohne Text kein Name')
})
test('Sondertag mit Modus zu, rund und nach Vereinbarung', () => {
const p = plan({ 1: ['09:00', '18:00'] })
for (const modus of ['zu', 'rund', 'vereinbarung']) {
const d = daten({ plaene: [p], ausnahmen: [sondertag({ von: '2026-10-05', bis: '2026-10-05', modus, fenster: [] })] })
const t = tagesbild(d, p, '2026-10-05')
assert.equal(t.art, 'sondertag')
assert.equal(t.modus, modus)
assert.deepEqual(t.fenster, [])
}
})
test('Ausnahme gilt von bis bis einschließlich, Grenzen inklusive', () => {
const p = plan({ 1: ['09:00', '18:00'], 2: ['09:00', '18:00'], 3: ['09:00', '18:00'], 4: ['09:00', '18:00'] })
const d = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-06', bis: '2026-10-07' })] })
assert.equal(tagesbild(d, p, '2026-10-05').art, 'regulaer')
assert.equal(tagesbild(d, p, '2026-10-06').art, 'urlaub')
assert.equal(tagesbild(d, p, '2026-10-07').art, 'urlaub')
assert.equal(tagesbild(d, p, '2026-10-08').art, 'regulaer')
})
test('planbezogene Ausnahme schlägt globale gleicher Art', () => {
const sommer = plan({ 1: ['09:00', '18:00'] }, { id: 'p-sommer', kurz: 'sommer', haupt: false })
const haupt = plan({ 1: ['09:00', '18:00'] }, { id: 'p-haupt', kurz: 'standard', haupt: true })
const ausnahmen = [
// planbezogene Ausnahme steht VOR der globalen, schlägt sie trotzdem
urlaub({ id: 'u1', von: '2026-10-05', bis: '2026-10-09', text: 'Nur Sommerplan', plan: 'p-sommer' }),
urlaub({ id: 'u2', von: '2026-10-05', bis: '2026-10-09', text: 'Alle Pläne', plan: null }),
]
const d = daten({ plaene: [haupt, sommer], ausnahmen })
assert.equal(tagesbild(d, sommer, '2026-10-05').name, 'Nur Sommerplan')
assert.equal(tagesbild(d, haupt, '2026-10-05').name, 'Alle Pläne')
})
test('Ausnahme eines anderen Plans gilt nicht', () => {
const sommer = plan({ 1: ['09:00', '18:00'] }, { id: 'p-sommer', kurz: 'sommer', haupt: false })
const haupt = plan({ 1: ['09:00', '18:00'] }, { id: 'p-haupt', kurz: 'standard', haupt: true })
const d = daten({ plaene: [haupt, sommer], ausnahmen: [urlaub({ von: '2026-10-05', bis: '2026-10-05', plan: 'p-sommer' })] })
assert.equal(tagesbild(d, sommer, '2026-10-05').art, 'urlaub')
assert.equal(tagesbild(d, haupt, '2026-10-05').art, 'regulaer')
})
test('Die Art schlägt die Planbezogenheit: globaler Sondertag vor planbezogenem Urlaub', () => {
const haupt = plan({ 1: ['09:00', '18:00'] }, { id: 'p-haupt', kurz: 'standard', haupt: true })
const d = daten({
plaene: [haupt],
ausnahmen: [
urlaub({ id: 'u1', von: '2026-10-05', bis: '2026-10-05', plan: 'p-haupt' }),
sondertag({ id: 's1', von: '2026-10-05', bis: '2026-10-05', plan: null, text: 'Alle' }),
],
})
assert.equal(tagesbild(d, haupt, '2026-10-05').art, 'sondertag')
})
test('zwei überlappende Urlaube: der später angelegte gilt', () => {
const p = plan({ 1: ['09:00', '18:00'] })
const a = urlaub({ id: 'a', von: '2026-10-05', bis: '2026-10-12', text: 'Erster' })
const b = urlaub({ id: 'b', von: '2026-10-05', bis: '2026-10-07', text: 'Zweiter' })
assert.equal(tagesbild(daten({ plaene: [p], ausnahmen: [a, b] }), p, '2026-10-05').name, 'Zweiter')
assert.equal(tagesbild(daten({ plaene: [p], ausnahmen: [b, a] }), p, '2026-10-05').name, 'Erster', 'höherer Index = später angelegt')
assert.equal(tagesbild(daten({ plaene: [p], ausnahmen: [a, b] }), p, '2026-10-10').name, 'Erster', 'außerhalb des späteren gilt der frühere')
})
test('zwei überlappende Sondertage: der später angelegte gilt', () => {
const p = plan({ 1: ['09:00', '18:00'] })
const a = sondertag({ id: 'a', von: '2026-10-05', bis: '2026-10-05', text: 'Erster', fenster: [['10:00', '12:00']] })
const b = sondertag({ id: 'b', von: '2026-10-05', bis: '2026-10-05', text: 'Zweiter', fenster: [['15:00', '17:00']] })
const t = tagesbild(daten({ plaene: [p], ausnahmen: [a, b] }), p, '2026-10-05')
assert.equal(t.name, 'Zweiter')
assert.deepEqual(t.fenster, [['15:00', '17:00']])
})
test('Urlaub über den Jahreswechsel', () => {
const p = plan({ 6: ['10:00', '16:00'], 7: ['10:00', '16:00'], 1: ['09:00', '18:00'] })
const d = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-12-28', bis: '2027-01-03' })] })
for (const tag of ['2026-12-28', '2026-12-31', '2027-01-01', '2027-01-02', '2027-01-03']) {
assert.equal(tagesbild(d, p, tag).art, 'urlaub', tag)
}
assert.equal(tagesbild(d, p, '2026-12-27').art, 'regulaer')
assert.equal(tagesbild(d, p, '2027-01-04').art, 'regulaer')
})
test('Ausnahmen mit fehlendem oder umgekehrtem Zeitraum werden ignoriert, fehlendes bis = ein Tag', () => {
const p = plan({ 1: ['09:00', '18:00'], 2: ['09:00', '18:00'] })
const d = daten({
plaene: [p],
ausnahmen: [
{ id: 'x', art: 'urlaub', von: '2026-10-06', bis: '2026-10-05', modus: 'zu', fenster: [], plan: null },
{ id: 'y', art: 'urlaub', von: undefined, bis: undefined, modus: 'zu', fenster: [], plan: null },
{ id: 'z', art: 'ferien', von: '2026-10-05', bis: '2026-10-05', modus: 'zu', fenster: [], plan: null },
{ id: 'w', art: 'urlaub', von: '2026-10-06', text: 'ein Tag', modus: 'zu', fenster: [], plan: null },
null,
],
})
assert.equal(tagesbild(d, p, '2026-10-05').art, 'regulaer')
assert.equal(tagesbild(d, p, '2026-10-06').art, 'urlaub')
assert.equal(tagesbild(d, p, '2026-10-07').art, 'regulaer')
})
test('tagesbild rechnet nichts in die Daten zurück und wirft bei ungültigem Tag', () => {
const d = tiefEinfrieren(daten({ ausnahmen: [urlaub({ von: '2026-10-06', bis: '2026-10-07' })], feiertage: { '2026-10-03': { modus: 'zeiten', fenster: [['10:00', '14:00']] } } }))
const p = hauptplan(d)
assert.doesNotThrow(() => tagesbild(d, p, '2026-10-03'))
assert.doesNotThrow(() => tagesbild(d, p, '2026-10-06'))
assert.doesNotThrow(() => oeffnungsIntervalle(d, p, '2026-10-01', '2026-10-10'))
for (const tag of [undefined, null, '', '2026-10-3', '03.10.2026', '2026-02-30', 5]) {
assert.throws(() => tagesbild(d, p, tag), RangeError, String(tag))
}
})
// ---------------------------------------------------------------- oeffnungsIntervalle
test('einfaches Fenster wird ein Intervall in Berliner Zeit', () => {
const d = daten()
const r = oeffnungsIntervalle(d, hauptplan(d), '2026-10-05', '2026-10-05')
assert.deepEqual(r, [{ vonMs: Date.parse('2026-10-05T07:00:00.000Z'), bisMs: Date.parse('2026-10-05T16:00:00.000Z') }])
})
test('zwei Fenster am Tag bleiben getrennt, Wochen über mehrere Tage', () => {
const p = plan({ 1: [['09:00', '12:30'], ['13:30', '18:00']], 2: ['09:00', '12:00'] })
const d = daten({ plaene: [p] })
assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-05', '2026-10-11')), [
['2026-10-05 09:00', '2026-10-05 12:30'],
['2026-10-05 13:30', '2026-10-05 18:00'],
['2026-10-06 09:00', '2026-10-06 12:00'],
])
})
test('aneinander grenzende Fenster werden zusammengefügt', () => {
const p = plan({ 1: [['09:00', '12:00'], ['12:00', '18:00']] })
const d = daten({ plaene: [p] })
assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-05', '2026-10-05')), [['2026-10-05 09:00', '2026-10-05 18:00']])
})
test('Fenster über Mitternacht gehört zum Starttag', () => {
// Bar: Samstag 18:00–02:00; Sonntag ist (in Brandenburg) Feiertag, Ostersonntag 2026-04-05
const p = plan({ 6: ['18:00', '02:00'] })
const d = daten({ land: 'BB', plaene: [p] })
assert.equal(tagesbild(d, p, '2026-04-05').art, 'feiertag')
const r = oeffnungsIntervalle(d, p, '2026-04-04', '2026-04-04')
assert.equal(r.length, 1)
assert.equal(new Date(r[0].vonMs).toISOString(), '2026-04-04T16:00:00.000Z')
assert.equal(new Date(r[0].bisMs).toISOString(), '2026-04-05T00:00:00.000Z')
assert.deepEqual(iv(r), [['2026-04-04 18:00', '2026-04-05 02:00']])
// auch wenn der Folgetag Urlaub oder ein geschlossener Sondertag ist
const urlaubSo = daten({ land: 'RP', plaene: [p], ausnahmen: [urlaub({ von: '2026-10-11', bis: '2026-10-11' })] })
assert.deepEqual(iv(oeffnungsIntervalle(urlaubSo, p, '2026-10-10', '2026-10-11')), [['2026-10-10 18:00', '2026-10-11 02:00']])
})
test('Intervalle enthalten das in vonTag hineinlaufende Fenster des Vortags', () => {
const p = plan({ 6: ['18:00', '02:00'] })
const d = daten({ plaene: [p] })
assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-11', '2026-10-11')), [['2026-10-10 18:00', '2026-10-11 02:00']])
assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-12', '2026-10-12')), [], 'Fenster endet vor vonTag: nicht dabei')
})
test('Fenster, das genau um 00:00 endet, läuft nicht in vonTag hinein', () => {
const p = plan({ 6: ['18:00', '24:00'] })
const d = daten({ plaene: [p] })
assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-11', '2026-10-11')), [])
assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-10', '2026-10-10')), [['2026-10-10 18:00', '2026-10-11 00:00']])
})
test('Nächtliches Fenster am letzten Tag des Bereichs läuft bis zu seinem Ende', () => {
const p = plan({ 6: ['18:00', '02:00'] })
const d = daten({ plaene: [p] })
assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-10', '2026-10-10')), [['2026-10-10 18:00', '2026-10-11 02:00']])
})
test('Mitternachtsfenster 18:00–00:00 und Folgetag ab 00:00 werden ein Intervall', () => {
const p = plan({ 1: ['18:00', '00:00'], 2: ['00:00', '06:00'] })
const d = daten({ plaene: [p] })
assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-05', '2026-10-06')), [['2026-10-05 18:00', '2026-10-06 06:00']])
})
test('rund um die Uhr an zwei Tagen wird ein Intervall', () => {
const p = plan({ 1: 'rund', 2: 'rund' })
const d = daten({ plaene: [p] })
const r = oeffnungsIntervalle(d, p, '2026-10-05', '2026-10-11')
assert.equal(r.length, 1)
assert.deepEqual(iv(r), [['2026-10-05 00:00', '2026-10-07 00:00']])
assert.equal(new Date(r[0].vonMs).toISOString(), '2026-10-04T22:00:00.000Z')
assert.equal(new Date(r[0].bisMs).toISOString(), '2026-10-06T22:00:00.000Z')
})
test('rund und ein Fenster am Folgetag werden zusammengefügt, nicht benachbarte bleiben getrennt', () => {
const p = plan({ 1: 'rund', 2: ['00:00', '08:00'], 4: 'rund' })
const d = daten({ plaene: [p] })
assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-05', '2026-10-11')), [
['2026-10-05 00:00', '2026-10-06 08:00'],
['2026-10-08 00:00', '2026-10-09 00:00'],
])
})
test('nach Vereinbarung und zu erzeugen kein Intervall', () => {
const p = plan({ 1: 'vereinbarung', 2: 'zu' })
const d = daten({ plaene: [p] })
assert.deepEqual(oeffnungsIntervalle(d, p, '2026-10-05', '2026-10-11'), [])
})
test('Feiertag, Urlaub und Sondertag wirken auf die Intervalle', () => {
const p = plan({ 1: ['09:00', '18:00'], 2: ['09:00', '18:00'], 3: ['09:00', '18:00'], 6: ['10:00', '16:00'] })
const d = daten({
plaene: [p],
ausnahmen: [urlaub({ von: '2026-10-06', bis: '2026-10-06' }), sondertag({ von: '2026-10-07', bis: '2026-10-07', fenster: [['12:00', '13:00']] })],
})
assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-03', '2026-10-07')), [
// 03.10. Feiertag (zu), 05.10. regulär, 06.10. Urlaub, 07.10. Sondertag
['2026-10-05 09:00', '2026-10-05 18:00'],
['2026-10-07 12:00', '2026-10-07 13:00'],
])
})
test('Feiertags-Abweichung öffnet an einem Feiertag', () => {
const d = daten({ feiertage: { '2026-10-03': { modus: 'zeiten', fenster: [['10:00', '14:00']] } } })
assert.deepEqual(iv(oeffnungsIntervalle(d, hauptplan(d), '2026-10-03', '2026-10-03')), [['2026-10-03 10:00', '2026-10-03 14:00']])
})
test('Sommerzeit: Fenster in der März-Lücke rückt auf 03:00, Länge in echter Zeit', () => {
// So 29.03.2026: 02:00–03:00 springt, Fenster 02:30–05:00 beginnt um 03:00 (01:00Z) und endet 05:00 CEST (03:00Z)
const p = plan({ 7: ['02:30', '05:00'] })
const d = daten({ plaene: [p] })
const r = oeffnungsIntervalle(d, p, '2026-03-29', '2026-03-29')
assert.equal(new Date(r[0].vonMs).toISOString(), '2026-03-29T01:00:00.000Z')
assert.equal(new Date(r[0].bisMs).toISOString(), '2026-03-29T03:00:00.000Z')
// Fenster, das ganz in der Lücke liegt, verschwindet
const lueckenfenster = plan({ 7: ['02:10', '02:50'] })
assert.deepEqual(oeffnungsIntervalle(daten({ plaene: [lueckenfenster] }), lueckenfenster, '2026-03-29', '2026-03-29'), [])
// Ende in der Lücke: bis 02:30 ⇒ 03:00
const endeInLuecke = plan({ 7: ['00:00', '02:30'] })
const r2 = oeffnungsIntervalle(daten({ plaene: [endeInLuecke] }), endeInLuecke, '2026-03-29', '2026-03-29')
assert.equal(new Date(r2[0].bisMs).toISOString(), '2026-03-29T01:00:00.000Z')
})
test('Sommerzeit: Tag „rund“ ist am Umstellungstag 23 bzw. 25 Stunden lang', () => {
const p = plan({ 7: 'rund' })
const d = daten({ plaene: [p] })
const stunden = (tag) => {
const [i] = oeffnungsIntervalle(d, p, tag, tag)
return (i.bisMs - i.vonMs) / 3600000
}
assert.equal(stunden('2026-03-29'), 23)
assert.equal(stunden('2026-10-25'), 25)
assert.equal(stunden('2026-10-18'), 24)
})
test('Oktober: Fenster 02:00–06:00 am Umstellungstag, doppelte Stunde zählt mit', () => {
const p = plan({ 7: ['02:00', '06:00'] })
const d = daten({ plaene: [p] })
const [i] = oeffnungsIntervalle(d, p, '2026-10-25', '2026-10-25')
assert.equal(new Date(i.vonMs).toISOString(), '2026-10-25T00:00:00.000Z', '02:00 Sommerzeit')
assert.equal(new Date(i.bisMs).toISOString(), '2026-10-25T05:00:00.000Z', '06:00 Winterzeit')
assert.equal((i.bisMs - i.vonMs) / 3600000, 5)
})
test('Nächtliches Fenster über die Zeitumstellung: Ende in der Wanduhr des Folgetags', () => {
const p = plan({ 6: ['22:00', '02:00'] }) // Sa 24.10. auf So 25.10. (Ende der Sommerzeit)
const d = daten({ plaene: [p] })
const [i] = oeffnungsIntervalle(d, p, '2026-10-24', '2026-10-24')
assert.equal(new Date(i.vonMs).toISOString(), '2026-10-24T20:00:00.000Z')
assert.equal(new Date(i.bisMs).toISOString(), '2026-10-25T00:00:00.000Z', '02:00 erstes Vorkommen = Sommerzeit')
})
test('Intervalle: Bereichsgrenzen, umgekehrter Bereich, ungültige Tage', () => {
const d = daten()
const p = hauptplan(d)
assert.deepEqual(oeffnungsIntervalle(d, p, '2026-10-06', '2026-10-05'), [])
assert.throws(() => oeffnungsIntervalle(d, p, 'x', '2026-10-05'), RangeError)
assert.throws(() => oeffnungsIntervalle(d, p, '2026-10-05', undefined), RangeError)
assert.throws(() => oeffnungsIntervalle(d, p, '2026-02-30', '2026-03-01'), RangeError)
})
test('Intervalle über 60 Tage und mehrere Jahre sind sortiert und überschneidungsfrei', () => {
const p = plan({ 1: [['09:00', '12:00'], ['13:00', '22:00']], 2: ['09:00', '12:00'], 4: ['20:00', '03:00'], 5: 'rund', 6: ['00:00', '08:00'] })
const d = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-12-28', bis: '2027-01-03' })] })
const r = oeffnungsIntervalle(d, p, '2026-12-01', '2027-02-15')
assert.ok(r.length > 20)
for (let k = 0; k < r.length; k += 1) {
assert.ok(r[k].bisMs > r[k].vonMs)
if (k > 0) assert.ok(r[k].vonMs > r[k - 1].bisMs, 'sortiert, mit Lücke (grenzende wurden zusammengefügt)')
}
// im Urlaub (28.12.–03.01.) beginnt kein Fenster, ein hineinlaufendes des 27.12. ist erlaubt
const imUrlaub = r.filter((i) => i.vonMs >= Date.parse('2026-12-27T23:00:00Z') && i.vonMs < Date.parse('2027-01-03T23:00:00Z'))
assert.deepEqual(imUrlaub, [])
})
test('Rückgabe der Intervalle ist reine Zahlen-Struktur ohne Seiteneffekt auf die Daten', () => {
const d = tiefEinfrieren(daten({ plaene: [plan({ 1: [['09:00', '12:00'], ['13:00', '18:00']], 6: ['22:00', '02:00'] })] }))
const r = oeffnungsIntervalle(d, hauptplan(d), '2026-10-05', '2026-10-11')
assert.ok(r.every((i) => Object.keys(i).sort().join() === 'bisMs,vonMs' && Number.isInteger(i.vonMs) && Number.isInteger(i.bisMs)))
})
// ---------------------------------------------------------------- Status und Texte (Task 5)
// Mo 05.10.2026 10:00 Berlin (MESZ)
const JETZT = new Date('2026-10-05T08:00:00.000Z')
const WOCHE_ALLE_RUND = { 1: 'rund', 2: 'rund', 3: 'rund', 4: 'rund', 5: 'rund', 6: 'rund', 7: 'rund' }
const WOCHE_ALLE_9_18 = { 1: ['09:00', '18:00'], 2: ['09:00', '18:00'], 3: ['09:00', '18:00'], 4: ['09:00', '18:00'], 5: ['09:00', '18:00'], 6: ['09:00', '18:00'], 7: ['09:00', '18:00'] }
const status = (d, tag, hhmm) => statusZu(d, hauptplan(d), zuUtc(tag, hhmm))
const nurStatus = (s) => ({ offen: s.offen, text: s.text, kurz: s.kurz })
test('uhrText: volle Stunde ohne Minuten, sonst H:MM, Mitternacht für 00:00 und 24:00', () => {
assert.equal(uhrText('09:00'), '9 Uhr')
assert.equal(uhrText('18:30'), '18:30 Uhr')
assert.equal(uhrText('09:05'), '9:05 Uhr')
assert.equal(uhrText('12:00'), '12 Uhr')
assert.equal(uhrText('23:55'), '23:55 Uhr')
assert.equal(uhrText('00:00'), 'Mitternacht')
assert.equal(uhrText('24:00'), 'Mitternacht')
assert.equal(uhrText('00:30'), '0:30 Uhr')
for (const falsch of ['', '9:00', '24:30', '25:00', '12:60', 'abc', null, undefined, 900]) {
assert.throws(() => uhrText(falsch), RangeError, String(falsch))
}
})
test('offen: schließt um 18 Uhr', () => {
const d = daten({ plaene: [plan({ 1: ['09:00', '18:00'] })] })
assert.deepEqual(statusZu(d, hauptplan(d), JETZT), { offen: true, text: 'Jetzt geöffnet · schließt um 18 Uhr', kurz: 'Geöffnet' })
// Grenzen: Beginn inklusive, Ende exklusive
assert.equal(status(d, '2026-10-05', '09:00').offen, true)
assert.equal(status(d, '2026-10-05', '17:59').offen, true)
assert.equal(status(d, '2026-10-05', '18:00').offen, false)
assert.equal(statusZu(d, hauptplan(d), new Date(zuUtc('2026-10-05', '09:00').getTime() - 1)).offen, false)
// Sekunden zählen mit: 17:59:59 ist noch offen
assert.equal(statusZu(d, hauptplan(d), new Date(zuUtc('2026-10-05', '18:00').getTime() - 1000)).offen, true)
})
test('offen: Schluss mit Minuten „schließt um 18:30 Uhr“, zwei Fenster am Tag', () => {
const d = daten({ plaene: [plan({ 1: [['09:00', '12:30'], ['13:30', '18:30']] })] })
assert.equal(status(d, '2026-10-05', '10:00').text, 'Jetzt geöffnet · schließt um 12:30 Uhr')
assert.equal(status(d, '2026-10-05', '12:30').text, 'Geschlossen · öffnet heute um 13:30 Uhr')
assert.equal(status(d, '2026-10-05', '14:00').text, 'Jetzt geöffnet · schließt um 18:30 Uhr')
})
test('Schluss um Mitternacht: „schließt um Mitternacht“ (00:00 und 24:00)', () => {
for (const ende of ['00:00', '24:00']) {
const d = daten({ plaene: [plan({ 1: ['18:00', ende] })] })
assert.deepEqual(status(d, '2026-10-05', '20:00'), { offen: true, text: 'Jetzt geöffnet · schließt um Mitternacht', kurz: 'Geöffnet' }, ende)
assert.equal(status(d, '2026-10-06', '00:00').offen, false, ende)
}
})
test('Schluss mehr als 24 h entfernt: „bis Dienstag 14 Uhr“', () => {
const d = daten({ plaene: [plan({ 1: ['10:00', '24:00'], 2: ['00:00', '14:00'] })] })
assert.deepEqual(statusZu(d, hauptplan(d), JETZT), { offen: true, text: 'Jetzt geöffnet · bis Dienstag 14 Uhr', kurz: 'Geöffnet' })
assert.equal(status(d, '2026-10-05', '13:59').text, 'Jetzt geöffnet · bis Dienstag 14 Uhr')
// genau 24 h vor dem Schluss ist es nicht „mehr als 24 h“
assert.equal(status(d, '2026-10-05', '14:00').text, 'Jetzt geöffnet · schließt um 14 Uhr')
assert.equal(status(d, '2026-10-06', '00:00').text, 'Jetzt geöffnet · schließt um 14 Uhr')
})
test('Schluss um Mitternacht mehr als 24 h entfernt: der Tag ist der, der endet („bis Dienstag Mitternacht“)', () => {
const d = daten({ plaene: [plan({ 1: ['10:00', '24:00'], 2: 'rund' })] })
assert.equal(statusZu(d, hauptplan(d), JETZT).text, 'Jetzt geöffnet · bis Dienstag Mitternacht')
})
test('Schluss mehr als 6 Tage entfernt: Datum statt Wochentag', () => {
// alles rund, aber am Mo 12.10. nur bis 09:00, danach Urlaub: Schluss in 6 Tagen 23 Stunden
const d = daten({
plaene: [plan(WOCHE_ALLE_RUND)],
ausnahmen: [sondertag({ id: 's', von: '2026-10-12', bis: '2026-10-12', fenster: [['00:00', '09:00']] }), urlaub({ von: '2026-10-13', bis: '2026-10-31' })],
})
assert.deepEqual(statusZu(d, hauptplan(d), JETZT), { offen: true, text: 'Jetzt geöffnet · bis 12.10. um 9 Uhr', kurz: 'Geöffnet' })
// Mitternacht: der endende Tag, nicht der Folgetag (Schluss Di 13.10. 00:00 = Montag Mitternacht)
const e = daten({ plaene: [plan(WOCHE_ALLE_RUND)], ausnahmen: [urlaub({ von: '2026-10-13', bis: '2026-10-31' })] })
assert.equal(status(e, '2026-10-06', '10:00').text, 'Jetzt geöffnet · bis Montag Mitternacht')
assert.equal(statusZu(e, hauptplan(e), JETZT).text, 'Rund um die Uhr geöffnet', 'ab Mo 10:00 sind es 7 Tage 14 Stunden')
})
test('7 Tage durchgehend: „Rund um die Uhr geöffnet“', () => {
const d = daten({ plaene: [plan(WOCHE_ALLE_RUND)] })
assert.deepEqual(statusZu(d, hauptplan(d), JETZT), { offen: true, text: 'Rund um die Uhr geöffnet', kurz: 'Geöffnet' })
// dasselbe als Fenster 00:00–24:00 an jedem Tag (grenzende Fenster werden zusammengefügt)
const e = daten({ plaene: [plan(Object.fromEntries([1, 2, 3, 4, 5, 6, 7].map((t) => [t, ['00:00', '24:00']])))] })
assert.equal(statusZu(e, hauptplan(e), JETZT).text, 'Rund um die Uhr geöffnet')
// genau 7 Tage ab jetzt (Mo 12.10. 10:00 Ende) zählt noch, eine Minute später nicht mehr
const f = daten({
plaene: [plan(WOCHE_ALLE_RUND)],
ausnahmen: [sondertag({ id: 's', von: '2026-10-12', bis: '2026-10-12', fenster: [['00:00', '10:00']] }), urlaub({ von: '2026-10-13', bis: '2026-10-31' })],
})
assert.equal(statusZu(f, hauptplan(f), JETZT).text, 'Rund um die Uhr geöffnet')
assert.equal(status(f, '2026-10-05', '10:01').text, 'Jetzt geöffnet · bis 12.10. um 10 Uhr')
})
test('6 Tage durchgehend sind nicht „rund um die Uhr“', () => {
const d = daten({ plaene: [plan({ 1: 'rund', 2: 'rund', 3: 'rund', 4: 'rund', 5: 'rund', 6: 'rund' })] })
assert.equal(statusZu(d, hauptplan(d), JETZT).text, 'Jetzt geöffnet · bis Samstag Mitternacht')
})
test('zu: öffnet heute / morgen / Wochentag / Datum', () => {
const zu = (woche, tag = '2026-10-05', uhr = '10:00') => nurStatus(status(daten({ plaene: [plan(woche)] }), tag, uhr))
assert.deepEqual(zu({ 1: ['14:00', '18:00'] }), { offen: false, text: 'Geschlossen · öffnet heute um 14 Uhr', kurz: 'Geschlossen' })
assert.equal(zu({ 2: ['09:00', '18:00'] }).text, 'Geschlossen · öffnet morgen um 9 Uhr')
assert.equal(zu({ 5: ['09:30', '18:00'] }).text, 'Geschlossen · öffnet Freitag um 9:30 Uhr')
assert.equal(zu({ 2: ['09:00', '18:00'] }, '2026-10-05', '20:00').text, 'Geschlossen · öffnet morgen um 9 Uhr')
// ≤ 6 Tage: Wochentag (So 11.10. = 6 Tage; Mo nach Di = 6 Tage)
assert.equal(zu({ 7: ['09:00', '18:00'] }).text, 'Geschlossen · öffnet Sonntag um 9 Uhr')
assert.equal(zu({ 1: ['09:00', '18:00'] }, '2026-10-06').text, 'Geschlossen · öffnet Montag um 9 Uhr')
// 7 Tage und mehr: Datum
assert.equal(zu({ 1: ['09:00', '18:00'] }, '2026-10-05', '19:00').text, 'Geschlossen · öffnet am 12.10. um 9 Uhr')
assert.equal(zu({ 1: ['09:00', '18:00'] }, '2026-10-05', '09:00').offen, true)
})
test('zu: Öffnung genau um Mitternacht heißt „0 Uhr“ (nicht „Mitternacht“, das wäre Tagesende)', () => {
const d = daten({ plaene: [plan({ 2: 'rund' })] })
assert.equal(statusZu(d, hauptplan(d), JETZT).text, 'Geschlossen · öffnet morgen um 0 Uhr')
const e = daten({ plaene: [plan({ 2: ['00:00', '06:00'] })] })
assert.equal(statusZu(e, hauptplan(e), JETZT).text, 'Geschlossen · öffnet morgen um 0 Uhr')
})
test('zu: nächste Öffnung nach Feiertag oder Urlaub wird gefunden, auch weit voraus', () => {
const d = daten({ plaene: [plan({ 1: ['09:00', '18:00'] })], ausnahmen: [urlaub({ von: '2026-10-12', bis: '2026-11-16' })] })
// Mo 05.10. 19:00: nächstes Montag 12.10. und 19.10. … alles Urlaub bis 16.11. -> Mo 23.11. = 49 Tage
assert.equal(status(d, '2026-10-05', '19:00').text, 'Geschlossen · öffnet am 23.11. um 9 Uhr')
})
test('keine Öffnung in 60 Tagen: „Derzeit geschlossen“', () => {
const d = daten({ plaene: [plan({})] })
assert.deepEqual(statusZu(d, hauptplan(d), JETZT), { offen: false, text: 'Derzeit geschlossen', kurz: 'Geschlossen' })
// Sondertag genau am 60. Tag (04.12.) wird noch gefunden, am 61. (05.12.) nicht
const am60 = daten({ plaene: [plan({})], ausnahmen: [sondertag({ von: '2026-12-04', bis: '2026-12-04', fenster: [['09:00', '12:00']] })] })
assert.equal(statusZu(am60, hauptplan(am60), JETZT).text, 'Geschlossen · öffnet am 04.12. um 9 Uhr')
const am61 = daten({ plaene: [plan({})], ausnahmen: [sondertag({ von: '2026-12-05', bis: '2026-12-05', fenster: [['09:00', '12:00']] })] })
assert.equal(statusZu(am61, hauptplan(am61), JETZT).text, 'Derzeit geschlossen')
// kein Plan: auch kein Fehler
assert.equal(statusZu({}, null, JETZT).text, 'Derzeit geschlossen')
})
test('Grund an geschlossenen Tagen: Betriebsurlaub bis …', () => {
const p = plan(WOCHE_ALLE_9_18)
const d = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-05', bis: '2026-10-17', text: 'Sommerpause' })] })
assert.deepEqual(statusZu(d, p, JETZT), { offen: false, text: 'Heute geschlossen (Betriebsurlaub bis 17.10.)', kurz: 'Geschlossen' })
// aneinander grenzende Urlaube zählen zusammen
const zwei = daten({ plaene: [p], ausnahmen: [urlaub({ id: 'u1', von: '2026-10-05', bis: '2026-10-10' }), urlaub({ id: 'u2', von: '2026-10-11', bis: '2026-10-17' })] })
assert.equal(statusZu(zwei, p, JETZT).text, 'Heute geschlossen (Betriebsurlaub bis 17.10.)')
// ein einzelner Urlaubstag
const eins = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-05', bis: '2026-10-05' })] })
assert.equal(statusZu(eins, p, JETZT).text, 'Heute geschlossen (Betriebsurlaub bis 05.10.)')
// ein geöffneter Sondertag mitten im Urlaub beendet die Kette
const luecke = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-05', bis: '2026-10-17' }), sondertag({ von: '2026-10-09', bis: '2026-10-09' })] })
assert.equal(statusZu(luecke, p, JETZT).text, 'Heute geschlossen (Betriebsurlaub bis 08.10.)')
// Urlaub eines anderen Plans zählt nicht
const fremd = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-05', bis: '2026-10-17', plan: 'anderer' })] })
assert.equal(statusZu(fremd, p, JETZT).text, 'Jetzt geöffnet · schließt um 18 Uhr')
// Urlaub ab morgen: heute normal
const morgen = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-06', bis: '2026-10-17' })] })
assert.equal(statusZu(morgen, p, JETZT).text, 'Jetzt geöffnet · schließt um 18 Uhr')
assert.equal(status(morgen, '2026-10-05', '19:00').text, 'Geschlossen · öffnet am 18.10. um 9 Uhr')
})
test('Grund an geschlossenen Tagen: Feiertag mit Namen (auch geschlossener Prüftag)', () => {
const p = plan(WOCHE_ALLE_9_18)
const d = daten({ plaene: [p] })
assert.deepEqual(status(d, '2026-10-03', '10:00'), { offen: false, text: 'Heute geschlossen (Feiertag: Tag der Deutschen Einheit)', kurz: 'Geschlossen' })
// Prüftag (R5): vom Kunden geschlossen ⇒ derselbe Grundtext
const heiligabend = daten({ plaene: [p], feiertage: { '2026-12-24': { modus: 'zu', fenster: [] } } })
assert.equal(status(heiligabend, '2026-12-24', '10:00').text, 'Heute geschlossen (Feiertag: Heiligabend)')
// Prüftag ohne Abweichung = normaler Wochentag
assert.equal(status(d, '2026-12-24', '10:00').text, 'Jetzt geöffnet · schließt um 18 Uhr')
// Feiertag mit Öffnungszeit (Abweichung): kein Grund, nach Schluss normaler Text
const offen = daten({ plaene: [p], feiertage: { '2026-10-03': { modus: 'zeiten', fenster: [['10:00', '14:00']] } } })
assert.equal(status(offen, '2026-10-03', '10:30').text, 'Jetzt geöffnet · schließt um 14 Uhr')
assert.equal(status(offen, '2026-10-03', '15:00').text, 'Geschlossen · öffnet morgen um 9 Uhr')
assert.equal(status(offen, '2026-10-03', '09:00').text, 'Geschlossen · öffnet heute um 10 Uhr')
})
test('Grund: regulär geschlossene Tage und geschlossene Sondertage haben keinen Grundtext', () => {
const d = daten({ plaene: [plan({ 2: ['09:00', '18:00'] })] })
assert.equal(statusZu(d, hauptplan(d), JETZT).text, 'Geschlossen · öffnet morgen um 9 Uhr')
const s = daten({ plaene: [plan(WOCHE_ALLE_9_18)], ausnahmen: [sondertag({ von: '2026-10-05', bis: '2026-10-05', text: 'Inventur', modus: 'zu', fenster: [] })] })
assert.equal(statusZu(s, hauptplan(s), JETZT).text, 'Geschlossen · öffnet morgen um 9 Uhr')
})
test('Urlaub: ein vom Vortag hineinlaufendes Fenster hält offen, danach „Heute geschlossen“', () => {
// Bar: Freitag 18–02, Samstag ist Betriebsurlaub
const d = daten({ plaene: [plan({ 5: ['18:00', '02:00'], 6: ['18:00', '02:00'] })], ausnahmen: [urlaub({ von: '2026-10-10', bis: '2026-10-11' })] })
assert.deepEqual(status(d, '2026-10-10', '01:00'), { offen: true, text: 'Jetzt geöffnet · schließt um 2 Uhr', kurz: 'Geöffnet' })
assert.equal(status(d, '2026-10-10', '03:00').text, 'Heute geschlossen (Betriebsurlaub bis 11.10.)')
})
test('nach Vereinbarung: „Heute nach Vereinbarung“ mit offen: null', () => {
const d = daten({ plaene: [plan({ 1: 'vereinbarung', 2: ['09:00', '18:00'] })] })
assert.deepEqual(statusZu(d, hauptplan(d), JETZT), { offen: null, text: 'Heute nach Vereinbarung', kurz: 'Nach Vereinbarung' })
// Vereinbarung an einem Feiertag (Abweichung)
const f = daten({ plaene: [plan(WOCHE_ALLE_9_18)], feiertage: { '2026-10-03': { modus: 'vereinbarung', fenster: [] } } })
assert.deepEqual(status(f, '2026-10-03', '10:00'), { offen: null, text: 'Heute nach Vereinbarung', kurz: 'Nach Vereinbarung' })
// ein vom Vortag hineinlaufendes Fenster ist weiter „offen“, danach Vereinbarung
const n = daten({ plaene: [plan({ 7: ['20:00', '02:00'], 1: 'vereinbarung' })] })
assert.equal(status(n, '2026-10-05', '01:00').offen, true)
assert.equal(status(n, '2026-10-05', '02:00').offen, null)
// Vereinbarung erst morgen ändert heute nichts
const m = daten({ plaene: [plan({ 2: 'vereinbarung', 3: ['09:00', '18:00'] })] })
assert.deepEqual(nurStatus(statusZu(m, hauptplan(m), JETZT)), { offen: false, text: 'Geschlossen · öffnet Mittwoch um 9 Uhr', kurz: 'Geschlossen' })
})
test('Fenster 18–02: um 01:00 am Folgetag offen, ab 02:00 zu', () => {
const d = daten({ plaene: [plan({ 6: ['18:00', '02:00'] })] })
assert.deepEqual(status(d, '2026-10-11', '01:00'), { offen: true, text: 'Jetzt geöffnet · schließt um 2 Uhr', kurz: 'Geöffnet' })
assert.equal(status(d, '2026-10-11', '01:59').offen, true)
assert.equal(status(d, '2026-10-11', '02:00').offen, false)
assert.equal(status(d, '2026-10-10', '20:00').text, 'Jetzt geöffnet · schließt um 2 Uhr')
// Bar: Samstag 18–02, Sonntag ist Betriebsurlaub ⇒ bis 02:00 trotzdem offen
const f = daten({ plaene: [plan({ 6: ['18:00', '02:00'] })], ausnahmen: [urlaub({ von: '2026-10-11', bis: '2026-10-11' })] })
assert.equal(status(f, '2026-10-11', '01:30').offen, true)
})
test('Sommerzeit 25.10.: Fenster 02:00–06:00 ⇒ „schließt um 6 Uhr“ (beide 02:30-Zeitpunkte)', () => {
const d = daten({ plaene: [plan({ 7: ['02:00', '06:00'] })] })
// 02:30 MESZ (erstes Vorkommen) und 02:30 MEZ (zweites Vorkommen)
assert.deepEqual(statusZu(d, hauptplan(d), new Date('2026-10-25T00:30:00Z')), { offen: true, text: 'Jetzt geöffnet · schließt um 6 Uhr', kurz: 'Geöffnet' })
assert.equal(statusZu(d, hauptplan(d), new Date('2026-10-25T01:30:00Z')).text, 'Jetzt geöffnet · schließt um 6 Uhr')
assert.equal(statusZu(d, hauptplan(d), new Date('2026-10-25T04:59:00Z')).offen, true)
assert.equal(statusZu(d, hauptplan(d), new Date('2026-10-25T05:00:00Z')).offen, false)
// eine Minute vor Beginn (01:59 MESZ)
assert.equal(statusZu(d, hauptplan(d), new Date('2026-10-24T23:59:00Z')).text, 'Geschlossen · öffnet heute um 2 Uhr')
})
test('Sommerzeit 29.03.: Fenster 02:00–06:00 beginnt in der Lücke um 03:00', () => {
const d = daten({ plaene: [plan({ 7: ['02:00', '06:00'] })] })
assert.equal(status(d, '2026-03-28', '10:00').text, 'Geschlossen · öffnet morgen um 3 Uhr')
assert.equal(status(d, '2026-03-29', '03:30').text, 'Jetzt geöffnet · schließt um 6 Uhr')
})
test('nie „24 Uhr“ in irgendeinem Text (jede Viertelstunde einer Woche, rund/24h-Pläne)', () => {
const plaene = [
plan(WOCHE_ALLE_RUND),
plan(Object.fromEntries([1, 2, 3, 4, 5, 6, 7].map((t) => [t, ['00:00', '24:00']]))),
plan({ 1: ['18:00', '24:00'], 2: ['00:00', '02:00'], 3: ['22:00', '00:00'], 5: 'rund', 6: 'rund', 7: ['06:00', '24:00'] }),
plan({ 1: ['00:00', '24:00'], 2: ['10:00', '24:00'], 3: ['00:00', '12:00'], 5: [['00:00', '08:00'], ['20:00', '24:00']] }),
]
const start = zuUtc('2026-10-05', '00:00').getTime()
const KONTROLLE = /24 Uhr/
for (const p of plaene) {
const d = daten({ plaene: [p] })
for (let k = 0; k < 7 * 96; k += 1) {
const jetzt = new Date(start + k * 15 * 60 * 1000)
const s = statusZu(d, p, jetzt)
assert.doesNotMatch(s.text, KONTROLLE, `${berlin(jetzt.getTime())}: ${s.text}`)
}
for (const w of wechselListe(d, p, new Date(start))) assert.doesNotMatch(w.text, KONTROLLE, w.text)
}
})
test('statusZu verändert die Daten nicht und prüft den Zeitpunkt', () => {
const d = tiefEinfrieren(daten({ plaene: [plan({ 1: ['09:00', '18:00'], 6: ['22:00', '02:00'] })], ausnahmen: [urlaub({ von: '2026-10-12', bis: '2026-10-14' })] }))
assert.equal(statusZu(d, hauptplan(d), JETZT).offen, true)
assert.equal(wechselListe(d, hauptplan(d), JETZT).length > 3, true)
assert.equal(tageListe(d, hauptplan(d), '2026-10-05', 3).length, 3)
for (const falsch of [undefined, null, '2026-10-05', 1759651200000, new Date('x')]) {
assert.throws(() => statusZu(d, hauptplan(d), falsch), RangeError, String(falsch))
assert.throws(() => wechselListe(d, hauptplan(d), falsch), RangeError, String(falsch))
}
})
// ---------------------------------------------------------------- Wechselliste
const wl = (d, now = JETZT) => wechselListe(d, hauptplan(d), now)
const kompakt = (liste) => liste.map((w) => [berlin(w.ab), w.text])
test('wechselListe: erster Eintrag ist jetzt (Beginn der Minute), danach Wechsel um 18:00', () => {
const d = daten({ plaene: [plan({ 1: ['09:00', '18:00'] })] })
const mitSekunden = new Date(JETZT.getTime() + 30500)
const liste = wl(d, mitSekunden)
assert.equal(liste[0].ab, JETZT.getTime())
assert.deepEqual(nurStatus(liste[0]), { offen: true, text: 'Jetzt geöffnet · schließt um 18 Uhr', kurz: 'Geöffnet' })
assert.equal(liste[1].ab, zuUtc('2026-10-05', '18:00').getTime())
assert.deepEqual(nurStatus(liste[1]), { offen: false, text: 'Geschlossen · öffnet am 12.10. um 9 Uhr', kurz: 'Geschlossen' })
assert.deepEqual(Object.keys(liste[0]), ['ab', 'tag', 'offen', 'text', 'kurz'])
assert.ok(liste.every((w) => Number.isInteger(w.ab)))
})
test('wechselListe: vollständige Liste für Mo 09–18, 8 Tage, nur Wechsel und Texttage', () => {
const d = daten({ plaene: [plan({ 1: ['09:00', '18:00'] })] })
assert.deepEqual(kompakt(wl(d)), [
['2026-10-05 10:00', 'Jetzt geöffnet · schließt um 18 Uhr'],
['2026-10-05 18:00', 'Geschlossen · öffnet am 12.10. um 9 Uhr'],
['2026-10-06 00:00', 'Geschlossen · öffnet Montag um 9 Uhr'],
['2026-10-11 00:00', 'Geschlossen · öffnet morgen um 9 Uhr'],
['2026-10-12 00:00', 'Geschlossen · öffnet heute um 9 Uhr'],
['2026-10-12 09:00', 'Jetzt geöffnet · schließt um 18 Uhr'],
['2026-10-12 18:00', 'Geschlossen · öffnet am 19.10. um 9 Uhr'],
['2026-10-13 00:00', 'Geschlossen · öffnet Montag um 9 Uhr'],
])
})
test('wechselListe: Mitternachtseintrag wechselt „morgen“ zu „heute“', () => {
const d = daten({ plaene: [plan({ 2: ['09:00', '18:00'] })] })
const liste = wl(d)
assert.equal(liste[0].text, 'Geschlossen · öffnet morgen um 9 Uhr')
assert.equal(liste[1].ab, zuUtc('2026-10-06', '00:00').getTime())
assert.equal(liste[1].text, 'Geschlossen · öffnet heute um 9 Uhr')
assert.equal(liste[1].offen, false)
assert.equal(liste[2].ab, zuUtc('2026-10-06', '09:00').getTime())
assert.equal(liste[2].offen, true)
})
test('wechselListe hat tag (Berliner Datum bei ab)', () => {
const d = daten({ plaene: [plan({ 6: ['22:00', '02:00'] })] })
const liste = wl(d, new Date('2026-10-10T20:30:00Z')) // Sa 22:30 Berlin, offen bis So 02:00
assert.equal(liste[0].tag, '2026-10-10')
assert.deepEqual(nurStatus(liste[0]), { offen: true, text: 'Jetzt geöffnet · schließt um 2 Uhr', kurz: 'Geöffnet' })
const zu = liste.find((w) => w.offen === false)
assert.equal(berlin(zu.ab), '2026-10-11 02:00')
assert.equal(zu.tag, '2026-10-11')
for (const w of liste) assert.equal(w.tag, ausUtc(new Date(w.ab)).tag)
// knapp vor Mitternacht: tag wechselt mit dem Mitternachtseintrag
const spaet = wl(daten({ plaene: [plan({})] }), new Date('2026-10-05T21:59:30Z')) // Mo 23:59 Berlin
assert.equal(spaet[0].tag, '2026-10-05')
})
test('wechselListe: strikt aufsteigend, jeder Eintrag ändert etwas, innerhalb von 8 Tagen', () => {
const p = plan({ 1: [['09:00', '12:00'], ['13:00', '22:00']], 2: ['09:00', '12:00'], 4: ['20:00', '03:00'], 5: 'rund', 6: 'vereinbarung', 7: ['10:00', '14:00'] })
const d = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-08', bis: '2026-10-09' })] })
const liste = wl(d)
assert.ok(liste.length > 10)
for (let k = 1; k < liste.length; k += 1) {
assert.ok(liste[k].ab > liste[k - 1].ab)
const a = liste[k - 1]
const b = liste[k]
assert.ok(a.offen !== b.offen || a.text !== b.text || a.kurz !== b.kurz, `Eintrag ${k} ändert nichts`)
}
const ende = zuUtc('2026-10-13', '10:00').getTime()
assert.ok(liste[liste.length - 1].ab < ende)
assert.ok(liste.every((w) => w.ab >= JETZT.getTime()))
})
test('wechselListe stimmt mit statusZu überein (offen und kurz zu jeder Viertelstunde, Text bei kurzen Läufen)', () => {
const p = plan({ 1: [['09:00', '12:00'], ['13:00', '22:00']], 2: ['09:00', '12:00'], 4: ['20:00', '03:00'], 6: 'vereinbarung', 7: ['10:00', '14:00'] })
const d = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-08', bis: '2026-10-09' })] })
const liste = wl(d)
const ende = zuUtc('2026-10-13', '10:00').getTime()
for (let ms = JETZT.getTime(); ms < ende; ms += 15 * 60 * 1000) {
let w = liste[0]
for (const e of liste) if (e.ab <= ms) w = e
const s = statusZu(d, p, new Date(ms))
assert.deepEqual(nurStatus(w), nurStatus(s), berlin(ms))
}
})
test('wechselListe: Mitternachten an der Zeitumstellung sind Berliner 00:00 (25.10. und 29.03.)', () => {
const d = daten({ plaene: [plan({ 1: ['09:00', '18:00'] })] })
const okt = wl(d, new Date('2026-10-23T08:00:00Z')).map((w) => w.ab)
assert.ok(okt.includes(Date.parse('2026-10-24T22:00:00Z')), '25.10. 00:00 MESZ')
assert.ok(okt.includes(Date.parse('2026-10-25T23:00:00Z')), '26.10. 00:00 MEZ')
const maerz = wl(d, new Date('2026-03-27T08:00:00Z')).map((w) => w.ab)
assert.ok(maerz.includes(Date.parse('2026-03-28T23:00:00Z')), '29.03. 00:00 MEZ')
assert.ok(maerz.includes(Date.parse('2026-03-29T22:00:00Z')), '30.03. 00:00 MESZ')
// 8 Tage über die Umstellung: Wanduhr-Ende, nicht 8 × 24 h
const spaetEintrag = wl(d, new Date('2026-10-23T08:00:00Z'))
assert.ok(spaetEintrag[spaetEintrag.length - 1].ab < Date.parse('2026-10-31T09:00:00Z'))
})
test('wechselListe: Plan rund um die Uhr hat genau einen Eintrag', () => {
const d = daten({ plaene: [plan(WOCHE_ALLE_RUND)] })
const liste = wl(d)
assert.equal(liste.length, 1)
assert.deepEqual(nurStatus(liste[0]), { offen: true, text: 'Rund um die Uhr geöffnet', kurz: 'Geöffnet' })
})
test('wechselListe: Urlaub, Feiertag und Vereinbarung erscheinen mit Grund, wenn der Text wechselt', () => {
const p = plan({ 1: ['09:00', '18:00'], 2: ['09:00', '18:00'], 3: 'vereinbarung', 4: ['09:00', '18:00'] })
const d = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-08', bis: '2026-10-09' })] })
const texte = wl(d).map((w) => w.text)
assert.ok(texte.includes('Heute nach Vereinbarung'))
assert.ok(texte.includes('Heute geschlossen (Betriebsurlaub bis 09.10.)'))
})
// ---------------------------------------------------------------- tageListe
test('tageListe liefert datum/art/name/modus/fenster für n Tage ab vonTag', () => {
const p = plan({ 1: ['09:00', '18:00'], 6: ['10:00', '14:00'] })
const d = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-06', bis: '2026-10-06', text: 'Brückentag' })] })
const liste = tageListe(d, p, '2026-10-02', 5)
assert.equal(liste.length, 5)
assert.deepEqual(liste.map((t) => t.datum), ['2026-10-02', '2026-10-03', '2026-10-04', '2026-10-05', '2026-10-06'])
assert.deepEqual(Object.keys(liste[0]), ['datum', 'art', 'name', 'modus', 'fenster'])
assert.deepEqual(liste[1], { datum: '2026-10-03', art: 'feiertag', name: 'Tag der Deutschen Einheit', modus: 'zu', fenster: [] })
assert.deepEqual(liste[3], { datum: '2026-10-05', art: 'regulaer', name: null, modus: 'zeiten', fenster: [['09:00', '18:00']] })
assert.deepEqual(liste[4], { datum: '2026-10-06', art: 'urlaub', name: 'Brückentag', modus: 'zu', fenster: [] })
// dieselben Werte wie tagesbild, nur heißt der Tag hier „datum“
for (const t of liste) {
const { tag, ...b } = tagesbild(d, p, t.datum)
const { datum, ...rest } = t
assert.equal(datum, tag)
assert.deepEqual(rest, b)
}
})
test('tageListe: 60 Tage über den Jahreswechsel, Randfälle und Fehler', () => {
const p = plan({ 1: ['09:00', '18:00'] })
const d = daten({ plaene: [p] })
const liste = tageListe(d, p, '2026-12-01', 60)
assert.equal(liste.length, 60)
assert.equal(liste[0].datum, '2026-12-01')
assert.equal(liste[59].datum, '2027-01-29')
assert.equal(new Set(liste.map((t) => t.datum)).size, 60)
assert.deepEqual(tageListe(d, p, '2026-12-01', 0), [])
assert.throws(() => tageListe(d, p, '2026-02-30', 3), RangeError)
assert.throws(() => tageListe(d, p, undefined, 3), RangeError)
assert.throws(() => tageListe(d, p, '2026-12-01', -1), RangeError)
assert.throws(() => tageListe(d, p, '2026-12-01', 1.5), RangeError)
assert.throws(() => tageListe(d, p, '2026-12-01', '5'), RangeError)
assert.throws(() => tageListe(d, p, '2026-12-01', 1000), RangeError)
// Kopie: Änderungen an den Fenstern der Liste verändern die Daten nicht
const l2 = tageListe(d, p, '2026-10-05', 1)
l2[0].fenster.push(['20:00', '21:00'])
assert.deepEqual(p.tage[1].fenster, [['09:00', '18:00']])
})
// ---------------------------------------------------------------- Hinweise (Task 6)
const hinweis = (id, art, von, bis, extra) => ({ id, art, text: `Text ${id}`, knopf: null, von, bis, ...extra })
test('HINWEIS_RANG: Wichtig > Urlaub > Aktion > Info, eingefroren', () => {
assert.deepEqual(HINWEIS_RANG, { wichtig: 4, urlaub: 3, aktion: 2, info: 1 })
assert.ok(Object.isFrozen(HINWEIS_RANG))
})
test('Hinweis-Zeitraum in Berliner Zeit über die Zeitumstellung', () => {
const iso = (ms) => new Date(ms).toISOString()
// Oktober: Ende der Sommerzeit am 25.10. (Tag hat 25 Stunden)
const z = hinweisZeitraum({ von: '2026-10-24', bis: '2026-10-25' })
assert.equal(iso(z.ab), '2026-10-23T22:00:00.000Z')
assert.equal(iso(z.bis), '2026-10-25T23:00:00.000Z')
// März: Beginn der Sommerzeit am 29.03.
const m = hinweisZeitraum({ von: '2026-03-28', bis: '2026-03-29' })
assert.equal(iso(m.ab), '2026-03-27T23:00:00.000Z')
assert.equal(iso(m.bis), '2026-03-29T22:00:00.000Z')
// ein einzelner Normaltag: von 00:00 bis 24:00
const e = hinweisZeitraum({ von: '2026-10-05', bis: '2026-10-05' })
assert.equal(iso(e.ab), '2026-10-04T22:00:00.000Z')
assert.equal(iso(e.bis), '2026-10-05T22:00:00.000Z')
})
test('Hinweis-Zeitraum: fehlendes von/bis = unbegrenzt (als Zahl), kaputte Tage = null', () => {
const ENDLOS = 8.64e15
assert.deepEqual(hinweisZeitraum({}), { ab: 0, bis: ENDLOS })
assert.deepEqual(hinweisZeitraum({ von: null, bis: '' }), { ab: 0, bis: ENDLOS })
assert.equal(new Date(hinweisZeitraum({ von: '2026-10-05' }).ab).toISOString(), '2026-10-04T22:00:00.000Z')
assert.equal(hinweisZeitraum({ von: '2026-10-05' }).bis, ENDLOS)
assert.equal(hinweisZeitraum({ bis: '2026-10-05' }).ab, 0)
assert.ok(Number.isFinite(new Date(ENDLOS).getTime()), 'ENDLOS ist noch ein gültiges Date')
for (const kaputt of [{ von: '2026-02-30' }, { von: 'morgen' }, { bis: '05.10.2026' }, { von: 20261005 }, null, undefined, 'x']) {
assert.equal(hinweisZeitraum(kaputt), null, JSON.stringify(kaputt))
}
})
test('gueltigeHinweise: vergangene fehlen, künftige ≤ 8 Tage sind dabei, Rang gesetzt', () => {
const d = daten({
hinweise: [
hinweis('alt', 'info', '2026-09-20', '2026-10-04'), // endete gestern 24:00
hinweis('heute', 'aktion', '2026-10-01', '2026-10-05'), // läuft bis Ende heute
hinweis('kommt', 'urlaub', '2026-10-13', '2026-10-26', { knopf: { art: 'link', ziel: 'https://example.org/x', text: 'Mehr' } }),
hinweis('spaet', 'wichtig', '2026-10-14', '2026-10-15'), // Beginn in 8 Tagen 14 Stunden
hinweis('offen', 'info', '2026-10-01', undefined), // ohne Ende
],
})
const liste = gueltigeHinweise(d, JETZT)
assert.deepEqual(liste.map((h) => h.text), ['Text heute', 'Text kommt', 'Text offen'], 'gespeicherte Reihenfolge')
assert.deepEqual(liste[1], {
art: 'urlaub',
text: 'Text kommt',
knopf: { art: 'link', ziel: 'https://example.org/x', text: 'Mehr' },
ab: Date.parse('2026-10-12T22:00:00Z'),
bis: Date.parse('2026-10-26T23:00:00Z'),
rang: 3,
})
assert.deepEqual(liste.map((h) => h.rang), [2, 3, 1])
assert.deepEqual(Object.keys(liste[0]), ['art', 'text', 'knopf', 'ab', 'bis', 'rang'], 'keine internen IDs')
assert.equal(liste[0].knopf, null)
assert.equal(liste[2].bis, 8.64e15)
// der Knopf ist eine Kopie
assert.notEqual(liste[1].knopf, d.hinweise[2].knopf)
})
test('gueltigeHinweise: Beginn genau 8 Tage voraus zählt, eine Minute später nicht', () => {
const d = daten({ hinweise: [hinweis('x', 'info', '2026-10-13', '2026-10-13')] })
// 05.10. 00:00 Berlin + 8 Tage = 13.10. 00:00 = Beginn des Hinweises
assert.equal(gueltigeHinweise(d, new Date('2026-10-04T22:00:00Z')).length, 1)
// 04.10. 23:59 Berlin + 8 Tage = 12.10. 23:59, der Hinweis beginnt eine Minute später
assert.equal(gueltigeHinweise(d, new Date('2026-10-04T21:59:00Z')).length, 0)
// innerhalb des Tages und nach dem Ende
assert.equal(gueltigeHinweise(d, new Date('2026-10-13T21:59:00Z')).length, 1, 'bis 13.10. 23:59 gültig')
assert.equal(gueltigeHinweise(d, new Date('2026-10-13T22:00:00Z')).length, 0, 'ab 14.10. 00:00 vorbei')
})
test('gueltigeHinweise: Tag rechnet in Berliner Zeit (Mitternacht über die Zeitumstellung)', () => {
const d = daten({ hinweise: [hinweis('x', 'wichtig', '2026-10-24', '2026-10-25')] })
assert.equal(gueltigeHinweise(d, new Date('2026-10-25T22:59:00Z')).length, 1, '25.10. 23:59 CET… noch gültig')
assert.equal(gueltigeHinweise(d, new Date('2026-10-25T23:00:00Z')).length, 0, '26.10. 00:00 Berlin: vorbei')
})
test('gueltigeHinweise: kaputte Einträge werden übersprungen, nichts wird verändert', () => {
const d = daten({
hinweise: [
null,
'text',
hinweis('ohnetext', 'info', '2026-10-01', '2026-10-31', { text: ' ' }),
hinweis('textzahl', 'info', '2026-10-01', '2026-10-31', { text: 5 }),
hinweis('artfalsch', 'dringend', '2026-10-01', '2026-10-31'),
hinweis('artproto', 'constructor', '2026-10-01', '2026-10-31'),
hinweis('tagkaputt', 'info', '2026-02-30', '2026-10-31'),
hinweis('umgekehrt', 'info', '2026-10-20', '2026-10-10'),
hinweis('knopfkaputt', 'info', '2026-10-01', '2026-10-31', { knopf: { art: 'script', ziel: 'x', text: 'y' } }),
hinweis('gut', 'wichtig', '2026-10-01', '2026-10-31'),
],
})
tiefEinfrieren(d)
const liste = gueltigeHinweise(d, JETZT)
assert.deepEqual(liste.map((h) => h.text), ['Text knopfkaputt', 'Text gut'])
assert.equal(liste[0].knopf, null, 'unbekannte Knopfart wird verworfen, der Hinweis bleibt')
assert.deepEqual(gueltigeHinweise({}, JETZT), [])
assert.deepEqual(gueltigeHinweise(null, JETZT), [])
assert.deepEqual(gueltigeHinweise({ hinweise: 'kaputt' }, JETZT), [])
assert.throws(() => gueltigeHinweise(d, new Date('x')), RangeError)
assert.throws(() => gueltigeHinweise(d, null), RangeError)
})
// ---------------------------------------------------------------- JSON-LD (Task 6)
const LD = (extra) => ({ '@type': 'OpeningHoursSpecification', ...extra })
test('JSON-LD: Mitternachtsfenster mit closes < opens', () => {
const d = daten({ plaene: [plan({ 1: [['09:00', '12:30'], ['13:30', '18:00']], 5: ['10:00', '24:00'], 6: ['18:00', '02:00'] })] })
const ld = jsonLdFuer(d, hauptplan(d), JETZT)
assert.deepEqual(ld.openingHoursSpecification, [
LD({ dayOfWeek: 'Monday', opens: '09:00', closes: '12:30' }),
LD({ dayOfWeek: 'Monday', opens: '13:30', closes: '18:00' }),
LD({ dayOfWeek: 'Friday', opens: '10:00', closes: '00:00' }),
LD({ dayOfWeek: 'Saturday', opens: '18:00', closes: '02:00' }),
])
// nie '24:00' im JSON-LD
assert.doesNotMatch(JSON.stringify(ld), /24:00/)
})
test('JSON-LD: rund um die Uhr 00:00–23:59, nach Vereinbarung fehlt', () => {
const d = daten({ plaene: [plan({ 1: 'rund', 2: 'vereinbarung', 3: ['00:00', '24:00'], 4: ['09:00', '18:00'] })] })
const ld = jsonLdFuer(d, hauptplan(d), JETZT)
assert.deepEqual(ld.openingHoursSpecification, [
LD({ dayOfWeek: 'Monday', opens: '00:00', closes: '23:59' }),
LD({ dayOfWeek: 'Wednesday', opens: '00:00', closes: '23:59' }), // Fenster 00:00–24:00 = rund
LD({ dayOfWeek: 'Thursday', opens: '09:00', closes: '18:00' }),
])
assert.ok(!ld.openingHoursSpecification.some((e) => e.dayOfWeek === 'Tuesday'))
// Tage ohne Öffnung (zu, fehlend) kommen nicht vor
const leer = jsonLdFuer(daten({ plaene: [plan({})] }), plan({}), JETZT)
assert.deepEqual(leer.openingHoursSpecification, [])
})
test('JSON-LD: Urlaub als Sonderzeit geschlossen 00:00–00:00 mit validFrom/validThrough', () => {
const d = daten({ plaene: [plan(WOCHE_ALLE_9_18)], ausnahmen: [urlaub({ von: '2026-10-12', bis: '2026-10-16' })] })
const ld = jsonLdFuer(d, hauptplan(d), JETZT)
// ein Eintrag für den ganzen Urlaub, dazu Allerheiligen (RP, So 01.11.) im Fenster der nächsten 60 Tage
assert.deepEqual(ld.specialOpeningHoursSpecification, [
LD({ validFrom: '2026-10-12', validThrough: '2026-10-16', opens: '00:00', closes: '00:00' }),
LD({ validFrom: '2026-11-01', validThrough: '2026-11-01', opens: '00:00', closes: '00:00' }),
])
})
test('JSON-LD: Sonderzeiten – Sondertag mit Zeiten, rund, Vereinbarung als geschlossen', () => {
const d = daten({
land: 'BE',
plaene: [plan(WOCHE_ALLE_9_18)],
ausnahmen: [
sondertag({ id: 's1', von: '2026-10-10', bis: '2026-10-10', fenster: [['10:00', '14:00'], ['18:00', '02:00']] }),
sondertag({ id: 's2', von: '2026-10-20', bis: '2026-10-20', modus: 'rund', fenster: [] }),
sondertag({ id: 's3', von: '2026-10-22', bis: '2026-10-22', modus: 'vereinbarung', fenster: [] }),
],
})
const ld = jsonLdFuer(d, hauptplan(d), JETZT)
assert.deepEqual(ld.specialOpeningHoursSpecification, [
LD({ validFrom: '2026-10-10', validThrough: '2026-10-10', opens: '10:00', closes: '14:00' }),
LD({ validFrom: '2026-10-10', validThrough: '2026-10-10', opens: '18:00', closes: '02:00' }),
LD({ validFrom: '2026-10-20', validThrough: '2026-10-20', opens: '00:00', closes: '23:59' }),
// „nach Vereinbarung“ hat in JSON-LD keine Form; sonst blieben die Wochenzeiten des Tages stehen
LD({ validFrom: '2026-10-22', validThrough: '2026-10-22', opens: '00:00', closes: '00:00' }),
])
})
test('JSON-LD: Sonderzeiten – nächste 60 Tage ab heute, gleiche aufeinanderfolgende Tage zusammengefasst', () => {
const d = daten({
land: 'BE', // BE: kein Feiertag zwischen 05.10. und 03.12.
plaene: [plan(WOCHE_ALLE_9_18)],
ausnahmen: [
sondertag({ id: 'gestern', von: '2026-10-04', bis: '2026-10-04' }),
urlaub({ id: 'laeuft', von: '2026-10-01', bis: '2026-10-07' }),
urlaub({ id: 'a', von: '2026-10-12', bis: '2026-10-13' }),
urlaub({ id: 'b', von: '2026-10-15', bis: '2026-10-16' }), // Lücke am 14.10.: eigener Eintrag
urlaub({ id: 'letzter', von: '2026-12-03', bis: '2026-12-03' }), // heute + 59
urlaub({ id: 'zuweit', von: '2026-12-04', bis: '2026-12-10' }), // heute + 60
],
})
const spans = jsonLdFuer(d, hauptplan(d), JETZT).specialOpeningHoursSpecification.map((e) => [e.validFrom, e.validThrough])
assert.deepEqual(spans, [
['2026-10-05', '2026-10-07'], // läuft schon: ab heute
['2026-10-12', '2026-10-13'],
['2026-10-15', '2026-10-16'],
['2026-12-03', '2026-12-03'],
])
})
test('JSON-LD: Urlaub und angrenzender Feiertag mit gleichen Zeiten werden ein Zeitraum', () => {
const d = daten({ plaene: [plan(WOCHE_ALLE_9_18)], ausnahmen: [urlaub({ von: '2026-10-30', bis: '2026-10-31' })] })
const spans = jsonLdFuer(d, hauptplan(d), JETZT).specialOpeningHoursSpecification.map((e) => [e.validFrom, e.validThrough])
assert.deepEqual(spans, [['2026-10-30', '2026-11-01']], 'Allerheiligen (RP) schließt direkt an')
})
test('JSON-LD: neuerBlock mit @context vom Server, ohne name/address/url', () => {
const d = daten({ plaene: [plan({ 1: ['09:00', '18:00'] })], ausnahmen: [urlaub({ von: '2026-10-12', bis: '2026-10-16' })] })
const ld = jsonLdFuer(d, hauptplan(d), JETZT)
assert.deepEqual(Object.keys(ld), ['openingHoursSpecification', 'specialOpeningHoursSpecification', 'neuerBlock'])
assert.equal(ld.neuerBlock['@context'], 'https://schema.org')
assert.equal(ld.neuerBlock['@type'], 'LocalBusiness')
assert.deepEqual(Object.keys(ld.neuerBlock), ['@context', '@type', 'openingHoursSpecification', 'specialOpeningHoursSpecification'])
assert.deepEqual(ld.neuerBlock.openingHoursSpecification, ld.openingHoursSpecification)
assert.deepEqual(ld.neuerBlock.specialOpeningHoursSpecification, ld.specialOpeningHoursSpecification)
for (const verboten of ['name', 'address', 'url']) assert.ok(!(verboten in ld.neuerBlock), verboten)
// als JSON ausgeliefert: dieselbe Form nach dem Rundlauf
assert.deepEqual(JSON.parse(JSON.stringify(ld)), ld)
})
test('JSON-LD: ohne Plan keine Angaben, Eingaben bleiben unverändert', () => {
const d = tiefEinfrieren(daten({ plaene: [plan({ 1: ['09:00', '18:00'], 6: ['18:00', '02:00'] })], ausnahmen: [urlaub({ von: '2026-10-12', bis: '2026-10-16' })] }))
for (const kein of [null, undefined, 'x']) {
const ld = jsonLdFuer(d, kein, JETZT)
assert.deepEqual(ld.openingHoursSpecification, [])
assert.deepEqual(ld.specialOpeningHoursSpecification, [])
assert.equal(ld.neuerBlock['@context'], 'https://schema.org')
}
assert.deepEqual(jsonLdFuer(d, hauptplan(d), JETZT), jsonLdFuer(d, hauptplan(d), JETZT))
assert.throws(() => jsonLdFuer(d, hauptplan(d), new Date('x')), RangeError)
})
test('JSON-LD: heute ist der Berliner Tag (kurz nach Mitternacht Berlin ist noch UTC-Vortag)', () => {
const d = daten({ land: 'BE', plaene: [plan(WOCHE_ALLE_9_18)], ausnahmen: [sondertag({ von: '2026-10-05', bis: '2026-10-05' })] })
// 05.10. 00:30 Berlin = 04.10. 22:30 UTC: der 05.10. ist heute und noch dabei
assert.equal(jsonLdFuer(d, hauptplan(d), new Date('2026-10-04T22:30:00Z')).specialOpeningHoursSpecification.length, 1)
// 06.10. 00:30 Berlin: der 05.10. ist vorbei
assert.equal(jsonLdFuer(d, hauptplan(d), new Date('2026-10-05T22:30:00Z')).specialOpeningHoursSpecification.length, 0)
})
// ---------------------------------------------------------------- Google-Text (Task 6)
test('Google-Text: Montag zuerst, „geschlossen“, Sonderzeiten danach', () => {
const d = daten({
plaene: [plan({ 1: [['09:00', '12:30'], ['13:30', '18:00']], 2: ['09:00', '18:00'], 3: 'rund', 4: 'vereinbarung', 5: ['10:00', '24:00'], 6: ['18:00', '02:00'] })],
ausnahmen: [
urlaub({ id: 'u', von: '2026-10-12', bis: '2026-10-16', text: 'Betriebsurlaub' }),
sondertag({ id: 's', von: '2026-10-10', bis: '2026-10-10', text: 'Tag der offenen Tür', fenster: [['10:00', '14:00']] }),
],
})
const text = googleText(d, hauptplan(d), JETZT)
assert.ok(text.startsWith('Montag: 09:00–12:30, 13:30–18:00\n'))
assert.equal(
text,
[
'Montag: 09:00–12:30, 13:30–18:00',
'Dienstag: 09:00–18:00',
'Mittwoch: rund um die Uhr',
'Donnerstag: nach Vereinbarung',
'Freitag: 10:00–00:00',
'Samstag: 18:00–02:00',
'Sonntag: geschlossen',
'',
'Sonderzeiten (nächste 60 Tage):',
'10.10.: 10:00–14:00 (Tag der offenen Tür)',
'12.10.–16.10.: geschlossen (Betriebsurlaub)',
'01.11.: geschlossen (Allerheiligen)',
].join('\n'),
)
assert.doesNotMatch(text, /24:00|24 Uhr/)
})
test('Google-Text: ohne Sonderzeiten genau sieben Zeilen, ohne Anhängsel', () => {
const d = daten({ land: 'BE', plaene: [plan({ 1: ['09:00', '18:00'] })] })
const text = googleText(d, hauptplan(d), JETZT)
assert.deepEqual(text.split('\n'), [
'Montag: 09:00–18:00',
'Dienstag: geschlossen',
'Mittwoch: geschlossen',
'Donnerstag: geschlossen',
'Freitag: geschlossen',
'Samstag: geschlossen',
'Sonntag: geschlossen',
])
})
test('Google-Text: Sonderzeiten fassen gleiche Tage mit gleichem Grund zusammen, Zeiten statt „geschlossen“', () => {
const d = daten({
land: 'BE',
plaene: [plan(WOCHE_ALLE_9_18)],
ausnahmen: [
urlaub({ id: 'u1', von: '2026-10-12', bis: '2026-10-14', text: 'Betriebsferien' }),
urlaub({ id: 'u2', von: '2026-10-15', bis: '2026-10-15', text: 'Inventur' }), // anderer Grund: eigene Zeile
urlaub({ id: 'u3', von: '2026-10-20', bis: '2026-10-20', text: '' }), // ohne Text: Vorgabe „Betriebsurlaub“
sondertag({ id: 's1', von: '2026-10-24', bis: '2026-10-25', text: '', modus: 'zeiten', fenster: [['10:00', '12:00'], ['14:00', '24:00']] }),
sondertag({ id: 's2', von: '2026-10-26', bis: '2026-10-26', text: 'Messe', modus: 'rund', fenster: [] }),
sondertag({ id: 's3', von: '2026-10-27', bis: '2026-10-27', text: 'Fortbildung', modus: 'vereinbarung', fenster: [] }),
// heute und das Ende des Fensters: 05.10. + 59 = 03.12.
sondertag({ id: 's4', von: '2026-10-05', bis: '2026-10-05', text: 'Heute', fenster: [['12:00', '13:00']] }),
sondertag({ id: 's5', von: '2026-12-03', bis: '2026-12-04', text: 'Ende', fenster: [['12:00', '13:00']] }),
sondertag({ id: 's6', von: '2026-10-04', bis: '2026-10-04', text: 'Gestern', fenster: [['12:00', '13:00']] }),
],
})
const zeilen = googleText(d, hauptplan(d), JETZT).split('\n').slice(9)
assert.deepEqual(zeilen, [
'05.10.: 12:00–13:00 (Heute)',
'12.10.–14.10.: geschlossen (Betriebsferien)',
'15.10.: geschlossen (Inventur)',
'20.10.: geschlossen (Betriebsurlaub)',
'24.10.–25.10.: 10:00–12:00, 14:00–00:00',
'26.10.: rund um die Uhr (Messe)',
'27.10.: nach Vereinbarung (Fortbildung)',
'03.12.: 12:00–13:00 (Ende)',
])
})
test('Google-Text: Fenster 00:00–24:00 heißt „rund um die Uhr“, ohne Plan leer, Eingaben unverändert', () => {
const d = tiefEinfrieren(daten({ plaene: [plan({ 1: ['00:00', '24:00'] })] }))
assert.equal(googleText(d, hauptplan(d), JETZT).split('\n')[0], 'Montag: rund um die Uhr')
assert.equal(googleText(d, null, JETZT), '')
assert.equal(googleText(d, undefined, JETZT), '')
assert.throws(() => googleText(d, hauptplan(d), new Date('x')), RangeError)
})
// ---------------------------------------------------------------- Erinnerungstage (Task 6)
// Mo 28.09.2026 10:00 Berlin: 1–7 Tage = 29.09. bis 05.10. (Sa 03.10. = Tag der Deutschen Einheit)
const MO_28_09 = new Date('2026-09-28T08:00:00Z')
const MIT_SAMSTAG = { 1: ['09:00', '18:00'], 6: ['09:00', '14:00'] }
test('Erinnerung: Feiertag in 1–7 Tagen, offener Wochentag', () => {
const d = daten({ plaene: [plan(MIT_SAMSTAG)] })
assert.deepEqual(erinnerungsTage(d, MO_28_09), [{ tag: '2026-10-03', name: 'Tag der Deutschen Einheit', art: 'feiertag' }])
})
test('Erinnerung: Grenzen 1 bis 7 Tage in Berliner Tagen', () => {
const d = daten({ plaene: [plan(MIT_SAMSTAG)] })
const tage = (iso) => erinnerungsTage(d, new Date(iso)).map((e) => e.tag)
assert.deepEqual(tage('2026-09-26T08:00:00Z'), ['2026-10-03'], 'in 7 Tagen (Sa 26.09. + 7)')
assert.deepEqual(tage('2026-09-25T08:00:00Z'), [], 'in 8 Tagen: noch nicht')
assert.deepEqual(tage('2026-10-02T08:00:00Z'), ['2026-10-03'], 'morgen')
assert.deepEqual(tage('2026-10-03T08:00:00Z'), [], 'heute: zu spät')
// 02.10. 23:30 Berlin (21:30 UTC): der Feiertag ist morgen; 03.10. 00:30 Berlin (22:30 UTC Vortag): heute
assert.deepEqual(tage('2026-10-02T21:30:00Z'), ['2026-10-03'])
assert.deepEqual(tage('2026-10-02T22:30:00Z'), [])
})
test('Erinnerung: nicht bei geschlossenem Tag, Ausnahme oder gespeicherter Abweichung', () => {
const jetzt = MO_28_09
// Samstag im Hauptplan ohnehin zu
assert.deepEqual(erinnerungsTage(daten({ plaene: [plan({ 1: ['09:00', '18:00'] })] }), jetzt), [])
// Ausnahme deckt den Tag ab: Urlaub, Sondertag (global oder für den Hauptplan)
const offen = plan(MIT_SAMSTAG)
assert.deepEqual(erinnerungsTage(daten({ plaene: [offen], ausnahmen: [urlaub({ von: '2026-10-01', bis: '2026-10-05' })] }), jetzt), [])
assert.deepEqual(erinnerungsTage(daten({ plaene: [offen], ausnahmen: [sondertag({ von: '2026-10-03', bis: '2026-10-03' })] }), jetzt), [])
assert.deepEqual(erinnerungsTage(daten({ plaene: [offen], ausnahmen: [sondertag({ von: '2026-10-03', bis: '2026-10-03', plan: 'p1' })] }), jetzt), [])
// eine Ausnahme nur für einen anderen Plan deckt den Hauptplan nicht ab
const anderer = plan({}, { id: 'p2', kurz: 'kueche', haupt: false })
assert.equal(
erinnerungsTage(daten({ plaene: [offen, anderer], ausnahmen: [sondertag({ von: '2026-10-03', bis: '2026-10-03', plan: 'p2' })] }), jetzt).length,
1,
)
// Ausnahme eines anderen Tages stört nicht
assert.equal(erinnerungsTage(daten({ plaene: [offen], ausnahmen: [urlaub({ von: '2026-10-04', bis: '2026-10-10' })] }), jetzt).length, 1)
// gespeicherte Abweichung für den Tag (egal welcher Modus)
for (const abw of [{ modus: 'zu', fenster: [] }, { modus: 'zeiten', fenster: [['10:00', '12:00']] }, { modus: 'vereinbarung', fenster: [] }]) {
assert.deepEqual(erinnerungsTage(daten({ plaene: [offen], feiertage: { '2026-10-03': abw } }), jetzt), [], abw.modus)
}
// Abweichung für einen anderen Tag zählt nicht
assert.equal(erinnerungsTage(daten({ plaene: [offen], feiertage: { '2026-10-04': { modus: 'zu', fenster: [] } } }), jetzt).length, 1)
})
test('Erinnerung: Wochentag nach Vereinbarung wird erinnert, ohne Hauptplan nicht', () => {
const ver = daten({ plaene: [plan({ 6: 'vereinbarung' })] })
assert.equal(erinnerungsTage(ver, MO_28_09).length, 1)
const rund = daten({ plaene: [plan({ 6: 'rund' })] })
assert.equal(erinnerungsTage(rund, MO_28_09).length, 1)
// kein Hauptplan (noch nichts gespeichert, nur Nebenpläne): keine Erinnerung
assert.deepEqual(erinnerungsTage(daten({ plaene: [] }), MO_28_09), [])
assert.deepEqual(erinnerungsTage(daten({ plaene: [plan(MIT_SAMSTAG, { haupt: false })] }), MO_28_09), [])
assert.deepEqual(erinnerungsTage({}, MO_28_09), [])
assert.deepEqual(erinnerungsTage(null, MO_28_09), [])
})
test('Erinnerung: Heiligabend als „pruefen“', () => {
// Mo 21.12.2026: 22.–28.12. enthält Do 24.12. (Heiligabend), 25./26.12. (1./2. Weihnachtstag)
const d = daten({ plaene: [plan({ 1: ['09:00', '18:00'], 4: ['09:00', '18:00'], 5: ['09:00', '18:00'], 6: ['09:00', '14:00'] })] })
const liste = erinnerungsTage(d, new Date('2026-12-21T08:00:00Z'))
assert.deepEqual(liste, [
{ tag: '2026-12-24', name: 'Heiligabend', art: 'pruefen' },
{ tag: '2026-12-25', name: '1. Weihnachtstag', art: 'feiertag' },
{ tag: '2026-12-26', name: '2. Weihnachtstag', art: 'feiertag' },
])
// Silvester und Neujahr über den Jahreswechsel
const s = erinnerungsTage(d, new Date('2026-12-28T08:00:00Z'))
assert.deepEqual(s.map((e) => [e.tag, e.art]), [['2026-12-31', 'pruefen'], ['2027-01-01', 'feiertag']])
})
test('Erinnerung: Bundesland aus den Daten, Prüftage regional (Mariä Himmelfahrt in BY)', () => {
// 15.08.2026 ist ein Samstag; Mo 10.08. + 5
const woche = { 6: ['09:00', '14:00'] }
const by = erinnerungsTage(daten({ land: 'BY', plaene: [plan(woche)] }), new Date('2026-08-10T08:00:00Z'))
assert.deepEqual(by, [{ tag: '2026-08-15', name: 'Mariä Himmelfahrt', art: 'pruefen' }])
assert.deepEqual(erinnerungsTage(daten({ land: 'BE', plaene: [plan(woche)] }), new Date('2026-08-10T08:00:00Z')), [])
// unbekanntes Land: Vorgabe RP (dort 03.10. Feiertag)
assert.equal(erinnerungsTage(daten({ land: 'XX', plaene: [plan(MIT_SAMSTAG)] }), MO_28_09).length, 1)
})
test('Erinnerung: Eingaben bleiben unverändert, ungültiges now wirft', () => {
const d = tiefEinfrieren(daten({ plaene: [plan(MIT_SAMSTAG)] }))
assert.equal(erinnerungsTage(d, MO_28_09).length, 1)
assert.throws(() => erinnerungsTage(d, new Date('x')), RangeError)
assert.throws(() => erinnerungsTage(d, undefined), RangeError)
})
test('Erinnerungsfenster 08:00–20:00 Berlin', () => {
// Winter (MEZ = UTC+1): 08:00 Berlin = 07:00 UTC
const winter = (hhmm) => imErinnerungsfenster(new Date(`2026-12-01T${hhmm}:00Z`))
assert.equal(winter('06:59'), false) // 07:59 Berlin
assert.equal(winter('07:00'), true) // 08:00
assert.equal(winter('18:59'), true) // 19:59
assert.equal(winter('19:00'), false) // 20:00
// Sommer (MESZ = UTC+2): 08:00 Berlin = 06:00 UTC
const sommer = (hhmm) => imErinnerungsfenster(new Date(`2026-07-01T${hhmm}:00Z`))
assert.equal(sommer('05:59'), false)
assert.equal(sommer('06:00'), true)
assert.equal(sommer('17:59'), true)
assert.equal(sommer('18:00'), false)
// Sekunden zählen nicht über die Minute hinaus
assert.equal(imErinnerungsfenster(new Date('2026-12-01T18:59:59Z')), true)
assert.equal(imErinnerungsfenster(new Date('2026-12-01T06:59:59Z')), false)
// Nacht und Mittag
assert.equal(imErinnerungsfenster(new Date('2026-12-01T00:00:00Z')), false)
assert.equal(imErinnerungsfenster(new Date('2026-12-01T11:00:00Z')), true)
assert.throws(() => imErinnerungsfenster(new Date('x')), RangeError)
assert.throws(() => imErinnerungsfenster(undefined), RangeError)
})
// ---------------------------------------------------------------- Reinheit
test('zeitlogik.js ist rein: keine Appwrite- und keine Config-Importe, keine Millisekunden-Tagesrechnung', () => {
const quelle = readFileSync(new URL('../server/services/zeiten/zeitlogik.js', import.meta.url), 'utf8')
const importe = [...quelle.matchAll(/^import[\s\S]*?from\s+['"]([^'"]+)['"]/gm)].map((m) => m[1])
assert.deepEqual(importe.sort(), ['../termine/feiertage.js', '../termine/zeit.js'])
assert.doesNotMatch(quelle, /86400000|864e5/, 'Kalendertage nie mit +86400000')
assert.doesNotMatch(quelle, /Date\.now|new Date\(\)/, 'now kommt immer als Parameter')
})