Öffnungszeiten: Live-Status, Texte und Wechselzeitpunkte
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0f25f7cb52
commit
59b0ea799d
2 files changed
+604
-2
No files matched your search
@@ -7,8 +7,12 @@ import {
|
||||
zuUtcOderDanach,
|
||||
tagesbild,
|
||||
oeffnungsIntervalle,
|
||||
uhrText,
|
||||
statusZu,
|
||||
wechselListe,
|
||||
tageListe,
|
||||
} from '../server/services/zeiten/zeitlogik.js'
|
||||
import { ausUtc } from '../server/services/termine/zeit.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.
|
||||
@@ -581,6 +585,424 @@ test('Rückgabe der Intervalle ist reine Zahlen-Struktur ohne Seiteneffekt auf d
|
||||
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']])
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Reinheit
|
||||
|
||||
test('zeitlogik.js ist rein: keine Appwrite- und keine Config-Importe, keine Millisekunden-Tagesrechnung', () => {
|
||||
|
||||
Reference in new issue
Block a user