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>
1452 lines
80 KiB
JavaScript
1452 lines
80 KiB
JavaScript
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')
|
||
})
|