Terminplaner: Zeit (Berlin, Umstellung) und Feiertage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a5e12d7f34
commit
310f8434d6
5 files changed
+357
No files matched your search
Generated
+10
@@ -13,6 +13,7 @@
|
|||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"feiertagejs": "1.5.1",
|
||||||
"jose": "^6.0.11",
|
"jose": "^6.0.11",
|
||||||
"node-appwrite": "^14.1.0",
|
"node-appwrite": "^14.1.0",
|
||||||
"nodemailer": "^9.0.3",
|
"nodemailer": "^9.0.3",
|
||||||
@@ -875,6 +876,15 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/feiertagejs": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/feiertagejs/-/feiertagejs-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-K+erJKNcWGMmy2j9Ayn0MeinEt6LUXyQlUCjIkVR22jlPHJcI42227O+KF5/AB8MURuTUqO/kQSJwYoK/JaQHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"feiertagejs": "1.5.1",
|
||||||
"jose": "^6.0.11",
|
"jose": "^6.0.11",
|
||||||
"node-appwrite": "^14.1.0",
|
"node-appwrite": "^14.1.0",
|
||||||
"nodemailer": "^9.0.3",
|
"nodemailer": "^9.0.3",
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { isHoliday } from 'feiertagejs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gesetzliche Feiertage je Bundesland (feiertagejs 1.5.1, Tage als Berliner Kalendertage).
|
||||||
|
* Reine Funktion ohne Zugriff auf Appwrite; die Betriebs-Einstellung (Feiertage sperren ja/nein) entscheidet der Aufrufer.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Die 16 Bundeslaender (Kuerzel wie in feiertagejs, ohne 'BUND'/'ALL'/'AUGSBURG'). */
|
||||||
|
export const BUNDESLAENDER = Object.freeze([
|
||||||
|
'BW', 'BY', 'BE', 'BB', 'HB', 'HH', 'HE', 'MV', 'NI', 'NW', 'RP', 'SL', 'SN', 'ST', 'SH', 'TH',
|
||||||
|
])
|
||||||
|
|
||||||
|
const TAG_RE = /^\d{4}-\d{2}-\d{2}$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ist tag ('YYYY-MM-DD') in diesem Bundesland ein gesetzlicher Feiertag?
|
||||||
|
* Unbekanntes oder leeres Land und ungueltiger Tag ergeben false (kein Fehler, der die oeffentliche Abfrage kippt).
|
||||||
|
*/
|
||||||
|
export function istFeiertag(tag, land) {
|
||||||
|
if (typeof tag !== 'string' || !TAG_RE.test(tag) || !BUNDESLAENDER.includes(land)) return false
|
||||||
|
try {
|
||||||
|
return isHoliday(tag, land)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Terminplaner-Zeit: Wanduhr Europe/Berlin <-> UTC, nur ueber Intl (kein Browser-Datum, keine Zusatzbibliothek).
|
||||||
|
* Tage sind 'YYYY-MM-DD'-Strings, Uhrzeiten 'HH:MM'; Kalenderrechnung (plusTage, wochentag) fasst nie einen Zeitpunkt an.
|
||||||
|
* Nicht existierende Wanduhrzeiten (Maerz-Umstellung) liefert zuUtc als null, doppelte (Oktober) als erstes Vorkommen.
|
||||||
|
* Vorlage fuer offsetMs: /root/reports/terminplaner-2026-10-02/spike-code/tz.mjs
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const TZ = 'Europe/Berlin'
|
||||||
|
|
||||||
|
const TAG_RE = /^\d{4}-\d{2}-\d{2}$/
|
||||||
|
const ZEIT_RE = /^\d{2}:\d{2}$/
|
||||||
|
const MONAT_RE = /^\d{4}-\d{2}$/
|
||||||
|
|
||||||
|
const WOCHENTAGE = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']
|
||||||
|
const MONATE = [
|
||||||
|
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||||
|
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
|
||||||
|
]
|
||||||
|
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
|
||||||
|
const formatierer = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: TZ,
|
||||||
|
hourCycle: 'h23',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
second: 'numeric',
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Versatz (Wanduhr minus UTC) in ms zum UTC-Zeitpunkt utcMs. */
|
||||||
|
function offsetMs(utcMs) {
|
||||||
|
const p = {}
|
||||||
|
for (const { type, value } of formatierer.formatToParts(utcMs)) p[type] = Number(value)
|
||||||
|
const alsUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second)
|
||||||
|
return alsUtc - Math.floor(utcMs / 1000) * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ist tag ein echter Kalendertag (kein 30.02.)? */
|
||||||
|
function gueltigerTag(tag) {
|
||||||
|
if (typeof tag !== 'string' || !TAG_RE.test(tag)) return false
|
||||||
|
const [y, m, d] = tag.split('-').map(Number)
|
||||||
|
return new Date(Date.UTC(y, m - 1, d)).toISOString().slice(0, 10) === tag
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berliner Wanduhr -> UTC-Zeitpunkt (Date). '24:00' = Folgetag 00:00.
|
||||||
|
* null bei ungueltiger Eingabe und bei nicht existierenden Zeiten (Luecke der Maerz-Umstellung);
|
||||||
|
* doppelte Zeiten (Oktober) ergeben das erste Vorkommen (Sommerzeit).
|
||||||
|
*/
|
||||||
|
export function zuUtc(tag, hhmm) {
|
||||||
|
if (!gueltigerTag(tag) || typeof hhmm !== 'string' || !ZEIT_RE.test(hhmm)) return null
|
||||||
|
const [h, mi] = hhmm.split(':').map(Number)
|
||||||
|
if (h > 24 || mi > 59 || (h === 24 && mi !== 0)) return null
|
||||||
|
const [y, mo, d] = tag.split('-').map(Number)
|
||||||
|
const wand = Date.UTC(y, mo - 1, d, h, mi) // die Wanduhr als ob UTC; 24:00 rollt auf den Folgetag
|
||||||
|
const treffer = [...new Set([wand - offsetMs(wand - 86400000), wand - offsetMs(wand + 86400000)])]
|
||||||
|
.filter((t) => t + offsetMs(t) === wand)
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
return treffer.length ? new Date(treffer[0]) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** UTC-Zeitpunkt -> Berliner Wanduhr { tag: 'YYYY-MM-DD', uhrzeit: 'HH:MM' } (Sekunden werden abgeschnitten). */
|
||||||
|
export function ausUtc(date) {
|
||||||
|
const t = new Date(date.getTime() + offsetMs(date.getTime()))
|
||||||
|
return {
|
||||||
|
tag: `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}-${pad(t.getUTCDate())}`,
|
||||||
|
uhrzeit: `${pad(t.getUTCHours())}:${pad(t.getUTCMinutes())}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ISO-Wochentag 1..7 (Montag = 1) eines 'YYYY-MM-DD'. */
|
||||||
|
export function wochentag(tag) {
|
||||||
|
const [y, m, d] = tag.split('-').map(Number)
|
||||||
|
const w = new Date(Date.UTC(y, m - 1, d)).getUTCDay()
|
||||||
|
return w === 0 ? 7 : w
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kalendertag + n Tage (n darf negativ sein), unabhaengig von der Zeitumstellung. */
|
||||||
|
export function plusTage(tag, n) {
|
||||||
|
const [y, m, d] = tag.split('-').map(Number)
|
||||||
|
return new Date(Date.UTC(y, m - 1, d + n)).toISOString().slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Berliner Kalendertag des Zeitpunkts now. */
|
||||||
|
export function heute(now = new Date()) {
|
||||||
|
return ausUtc(now).tag
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kalenderdaten eines Monats 'YYYY-MM': { label: 'Oktober 2026', ersterWochentag (ISO 1..7), anzahlTage,
|
||||||
|
* vorher, nachher ('YYYY-MM') }. Ungueltiger Monat => null.
|
||||||
|
*/
|
||||||
|
export function monatInfo(monat) {
|
||||||
|
if (typeof monat !== 'string' || !MONAT_RE.test(monat)) return null
|
||||||
|
const [y, m] = monat.split('-').map(Number)
|
||||||
|
if (m < 1 || m > 12) return null
|
||||||
|
const monatString = (jahr, nr) => {
|
||||||
|
const t = new Date(Date.UTC(jahr, nr - 1, 1))
|
||||||
|
return `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}`
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: `${MONATE[m - 1]} ${y}`,
|
||||||
|
ersterWochentag: wochentag(`${monat}-01`),
|
||||||
|
anzahlTage: new Date(Date.UTC(y, m, 0)).getUTCDate(),
|
||||||
|
vorher: monatString(y, m - 1),
|
||||||
|
nachher: monatString(y, m + 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 'Montag, 12.10.2026' (Berliner Wanduhr). */
|
||||||
|
export function datumLang(date) {
|
||||||
|
const { tag } = ausUtc(date)
|
||||||
|
return `${WOCHENTAGE[wochentag(tag) - 1]}, ${datumKurzVonTag(tag)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** '12.10.2026' (Berliner Wanduhr). */
|
||||||
|
export function datumKurz(date) {
|
||||||
|
return datumKurzVonTag(ausUtc(date).tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** '10:30' (Berliner Wanduhr). */
|
||||||
|
export function uhrzeit(date) {
|
||||||
|
return ausUtc(date).uhrzeit
|
||||||
|
}
|
||||||
|
|
||||||
|
function datumKurzVonTag(tag) {
|
||||||
|
const [y, m, d] = tag.split('-')
|
||||||
|
return `${d}.${m}.${y}`
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { test } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import {
|
||||||
|
TZ, zuUtc, ausUtc, wochentag, plusTage, heute, monatInfo, datumLang, datumKurz, uhrzeit,
|
||||||
|
} from '../server/services/termine/zeit.js'
|
||||||
|
import { BUNDESLAENDER, istFeiertag } from '../server/services/termine/feiertage.js'
|
||||||
|
|
||||||
|
const iso = (d) => d.toISOString()
|
||||||
|
|
||||||
|
test('Zeitzone ist Europe/Berlin', () => assert.equal(TZ, 'Europe/Berlin'))
|
||||||
|
|
||||||
|
test('zuUtc: normale Zeit im Sommer (UTC+2) und Winter (UTC+1)', () => {
|
||||||
|
assert.equal(iso(zuUtc('2026-10-12', '10:30')), '2026-10-12T08:30:00.000Z')
|
||||||
|
assert.equal(iso(zuUtc('2026-01-15', '10:30')), '2026-01-15T09:30:00.000Z')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('zuUtc: Umstellung im Maerz, nicht existierende Zeiten sind null', () => {
|
||||||
|
assert.equal(zuUtc('2026-03-29', '02:30'), null)
|
||||||
|
assert.equal(zuUtc('2026-03-29', '02:00'), null)
|
||||||
|
assert.equal(zuUtc('2026-03-29', '02:55'), null)
|
||||||
|
assert.equal(iso(zuUtc('2026-03-29', '01:55')), '2026-03-29T00:55:00.000Z')
|
||||||
|
assert.equal(iso(zuUtc('2026-03-29', '03:00')), '2026-03-29T01:00:00.000Z')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('zuUtc: Umstellung im Oktober, doppelte Zeit = erstes Vorkommen', () => {
|
||||||
|
assert.equal(iso(zuUtc('2026-10-25', '02:30')), '2026-10-25T00:30:00.000Z')
|
||||||
|
assert.equal(iso(zuUtc('2026-10-25', '02:00')), '2026-10-25T00:00:00.000Z')
|
||||||
|
assert.equal(iso(zuUtc('2026-10-25', '01:55')), '2026-10-24T23:55:00.000Z')
|
||||||
|
assert.equal(iso(zuUtc('2026-10-25', '03:00')), '2026-10-25T02:00:00.000Z')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('zuUtc: 24:00 ist der Folgetag 00:00 (auch an Umstelltagen)', () => {
|
||||||
|
assert.equal(iso(zuUtc('2026-10-12', '24:00')), '2026-10-12T22:00:00.000Z')
|
||||||
|
assert.equal(iso(zuUtc('2026-01-31', '24:00')), '2026-01-31T23:00:00.000Z')
|
||||||
|
assert.equal(iso(zuUtc('2026-12-31', '24:00')), '2026-12-31T23:00:00.000Z')
|
||||||
|
assert.equal(iso(zuUtc('2026-03-28', '24:00')), '2026-03-28T23:00:00.000Z')
|
||||||
|
assert.equal(iso(zuUtc('2026-03-29', '24:00')), '2026-03-29T22:00:00.000Z')
|
||||||
|
assert.equal(iso(zuUtc('2026-10-24', '24:00')), '2026-10-24T22:00:00.000Z')
|
||||||
|
assert.equal(iso(zuUtc('2026-10-25', '24:00')), '2026-10-25T23:00:00.000Z')
|
||||||
|
assert.deepEqual(ausUtc(zuUtc('2026-10-12', '24:00')), { tag: '2026-10-13', uhrzeit: '00:00' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('zuUtc: Tage haben 23 bzw. 25 Stunden an den Umstelltagen', () => {
|
||||||
|
const stunden = (tag) => (zuUtc(plusTage(tag, 1), '00:00') - zuUtc(tag, '00:00')) / 3600000
|
||||||
|
assert.equal(stunden('2026-03-29'), 23)
|
||||||
|
assert.equal(stunden('2026-10-25'), 25)
|
||||||
|
assert.equal(stunden('2026-10-12'), 24)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('zuUtc: ungueltige Eingaben ergeben null statt Fehler', () => {
|
||||||
|
for (const [tag, zeit] of [
|
||||||
|
['2026-02-30', '10:00'], ['2026-13-01', '10:00'], ['2026-10-12', '25:00'], ['2026-10-12', '10:60'],
|
||||||
|
['2026-10-12', '24:30'], ['2026-10-12', 'abc'], ['abc', '10:00'], ['2026-10-12', '9:00'], ['2026-1-2', '10:00'],
|
||||||
|
[undefined, '10:00'], ['2026-10-12', undefined], [null, null],
|
||||||
|
]) assert.equal(zuUtc(tag, zeit), null, `${tag} ${zeit}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('zuUtc: jedes 5-Minuten-Raster an den Umstelltagen hin und zurueck (nur Maerz-Luecke fehlt)', () => {
|
||||||
|
for (const tag of ['2026-03-29', '2026-10-25', '2026-10-12', '2026-01-15']) {
|
||||||
|
let vorher = -Infinity
|
||||||
|
for (let min = 0; min < 24 * 60; min += 5) {
|
||||||
|
const zeit = `${String(Math.floor(min / 60)).padStart(2, '0')}:${String(min % 60).padStart(2, '0')}`
|
||||||
|
const t = zuUtc(tag, zeit)
|
||||||
|
if (tag === '2026-03-29' && zeit >= '02:00' && zeit < '03:00') { assert.equal(t, null, `${tag} ${zeit}`); continue }
|
||||||
|
assert.ok(t instanceof Date, `${tag} ${zeit}`)
|
||||||
|
assert.deepEqual(ausUtc(t), { tag, uhrzeit: zeit }, `${tag} ${zeit}`)
|
||||||
|
assert.ok(t.getTime() > vorher, `${tag} ${zeit} aufsteigend`)
|
||||||
|
vorher = t.getTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ausUtc rueckwaerts: Sommer, Winter, Mitternacht, Umstellung', () => {
|
||||||
|
assert.deepEqual(ausUtc(new Date('2026-10-12T08:30:00Z')), { tag: '2026-10-12', uhrzeit: '10:30' })
|
||||||
|
assert.deepEqual(ausUtc(new Date('2026-01-15T09:30:00Z')), { tag: '2026-01-15', uhrzeit: '10:30' })
|
||||||
|
assert.deepEqual(ausUtc(new Date('2026-10-11T22:00:00Z')), { tag: '2026-10-12', uhrzeit: '00:00' })
|
||||||
|
assert.deepEqual(ausUtc(new Date('2026-10-11T21:59:00Z')), { tag: '2026-10-11', uhrzeit: '23:59' })
|
||||||
|
assert.deepEqual(ausUtc(new Date('2026-03-29T00:55:00Z')), { tag: '2026-03-29', uhrzeit: '01:55' })
|
||||||
|
assert.deepEqual(ausUtc(new Date('2026-03-29T01:00:00Z')), { tag: '2026-03-29', uhrzeit: '03:00' })
|
||||||
|
assert.deepEqual(ausUtc(new Date('2026-10-25T00:30:00Z')), { tag: '2026-10-25', uhrzeit: '02:30' })
|
||||||
|
assert.deepEqual(ausUtc(new Date('2026-10-25T01:30:00Z')), { tag: '2026-10-25', uhrzeit: '02:30' })
|
||||||
|
assert.deepEqual(ausUtc(new Date('2026-10-25T02:00:00Z')), { tag: '2026-10-25', uhrzeit: '03:00' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ausUtc: Sekunden werden abgeschnitten, nie aufgerundet', () => {
|
||||||
|
assert.deepEqual(ausUtc(new Date('2026-10-12T08:30:59.999Z')), { tag: '2026-10-12', uhrzeit: '10:30' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('wochentag: ISO 1..7, Montag = 1', () => {
|
||||||
|
assert.equal(wochentag('2026-10-05'), 1)
|
||||||
|
assert.equal(wochentag('2026-10-06'), 2)
|
||||||
|
assert.equal(wochentag('2026-10-10'), 6)
|
||||||
|
assert.equal(wochentag('2026-10-11'), 7)
|
||||||
|
assert.equal(wochentag('2026-02-01'), 7)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('plusTage: Kalendertage, unabhaengig von der Zeitumstellung', () => {
|
||||||
|
assert.equal(plusTage('2026-10-25', 1), '2026-10-26')
|
||||||
|
assert.equal(plusTage('2026-10-24', 1), '2026-10-25')
|
||||||
|
assert.equal(plusTage('2026-03-28', 1), '2026-03-29')
|
||||||
|
assert.equal(plusTage('2026-03-29', 1), '2026-03-30')
|
||||||
|
assert.equal(plusTage('2026-12-31', 1), '2027-01-01')
|
||||||
|
assert.equal(plusTage('2026-03-01', -1), '2026-02-28')
|
||||||
|
assert.equal(plusTage('2028-02-28', 1), '2028-02-29')
|
||||||
|
assert.equal(plusTage('2026-10-12', 0), '2026-10-12')
|
||||||
|
assert.equal(plusTage('2026-10-12', 30), '2026-11-11')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('heute: Berliner Kalendertag, nicht UTC-Tag', () => {
|
||||||
|
assert.equal(heute(new Date('2026-10-11T22:30:00Z')), '2026-10-12')
|
||||||
|
assert.equal(heute(new Date('2026-10-11T21:59:00Z')), '2026-10-11')
|
||||||
|
assert.equal(heute(new Date('2026-12-31T23:30:00Z')), '2027-01-01')
|
||||||
|
assert.equal(heute(new Date('2026-07-01T12:00:00Z')), '2026-07-01')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('monatInfo: Februar 2026 hat 28 Tage und beginnt am Sonntag', () => {
|
||||||
|
assert.deepEqual(monatInfo('2026-02'), {
|
||||||
|
label: 'Februar 2026', ersterWochentag: 7, anzahlTage: 28, vorher: '2026-01', nachher: '2026-03',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('monatInfo: Oktober 2026, Jahreswechsel, Schaltjahr', () => {
|
||||||
|
assert.deepEqual(monatInfo('2026-10'), {
|
||||||
|
label: 'Oktober 2026', ersterWochentag: 4, anzahlTage: 31, vorher: '2026-09', nachher: '2026-11',
|
||||||
|
})
|
||||||
|
assert.deepEqual(monatInfo('2026-12'), {
|
||||||
|
label: 'Dezember 2026', ersterWochentag: 2, anzahlTage: 31, vorher: '2026-11', nachher: '2027-01',
|
||||||
|
})
|
||||||
|
assert.deepEqual(monatInfo('2027-01'), {
|
||||||
|
label: 'Januar 2027', ersterWochentag: 5, anzahlTage: 31, vorher: '2026-12', nachher: '2027-02',
|
||||||
|
})
|
||||||
|
assert.equal(monatInfo('2028-02').anzahlTage, 29)
|
||||||
|
assert.equal(monatInfo('2026-03').label, 'März 2026')
|
||||||
|
assert.equal(monatInfo('2026-04').anzahlTage, 30)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('monatInfo: ungueltiger Monat ergibt null', () => {
|
||||||
|
for (const m of ['2026-00', '2026-13', '2026-1', '2026', 'abc', '', undefined, null, '2026-10-01']) {
|
||||||
|
assert.equal(monatInfo(m), null, String(m))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('datumLang, datumKurz, uhrzeit in Berliner Wanduhr', () => {
|
||||||
|
const d = new Date('2026-10-12T08:30:00Z')
|
||||||
|
assert.equal(datumLang(d), 'Montag, 12.10.2026')
|
||||||
|
assert.equal(datumKurz(d), '12.10.2026')
|
||||||
|
assert.equal(uhrzeit(d), '10:30')
|
||||||
|
const nachMitternacht = new Date('2026-10-11T22:30:00Z')
|
||||||
|
assert.equal(datumLang(nachMitternacht), 'Montag, 12.10.2026')
|
||||||
|
assert.equal(uhrzeit(nachMitternacht), '00:30')
|
||||||
|
assert.equal(datumLang(new Date('2026-03-04T10:00:00Z')), 'Mittwoch, 04.03.2026')
|
||||||
|
assert.equal(datumLang(new Date('2026-10-11T10:00:00Z')), 'Sonntag, 11.10.2026')
|
||||||
|
assert.equal(datumKurz(new Date('2026-01-05T10:00:00Z')), '05.01.2026')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Feiertage: 16 Bundeslaender', () => {
|
||||||
|
assert.equal(BUNDESLAENDER.length, 16)
|
||||||
|
assert.deepEqual([...BUNDESLAENDER].sort(), [
|
||||||
|
'BB', 'BE', 'BW', 'BY', 'HB', 'HE', 'HH', 'MV', 'NI', 'NW', 'RP', 'SH', 'SL', 'SN', 'ST', 'TH',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Feiertage: regionale Unterschiede', () => {
|
||||||
|
assert.equal(istFeiertag('2026-06-04', 'RP'), true) // Fronleichnam
|
||||||
|
assert.equal(istFeiertag('2026-06-04', 'BE'), false)
|
||||||
|
assert.equal(istFeiertag('2026-03-08', 'BE'), true) // Frauentag
|
||||||
|
assert.equal(istFeiertag('2026-03-08', 'NW'), false)
|
||||||
|
assert.equal(istFeiertag('2026-11-18', 'SN'), true) // Buss- und Bettag
|
||||||
|
assert.equal(istFeiertag('2026-11-18', 'BY'), false)
|
||||||
|
assert.equal(istFeiertag('2026-10-12', 'BY'), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Feiertage: 25.12. und 3.10. gelten in allen Bundeslaendern', () => {
|
||||||
|
for (const land of BUNDESLAENDER) {
|
||||||
|
assert.equal(istFeiertag('2026-12-25', land), true, `25.12. ${land}`)
|
||||||
|
assert.equal(istFeiertag('2026-10-03', land), true, `03.10. ${land}`)
|
||||||
|
assert.equal(istFeiertag('2026-10-13', land), false, `13.10. ${land}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Feiertage: unbekanntes Land oder ungueltiger Tag ist kein Feiertag, kein Fehler', () => {
|
||||||
|
assert.equal(istFeiertag('2026-12-25', 'XX'), false)
|
||||||
|
assert.equal(istFeiertag('2026-12-25', 'ALL'), false)
|
||||||
|
assert.equal(istFeiertag('2026-12-25', undefined), false)
|
||||||
|
assert.equal(istFeiertag('2026-12-25', null), false)
|
||||||
|
assert.equal(istFeiertag('foo', 'BE'), false)
|
||||||
|
assert.equal(istFeiertag(undefined, 'BE'), false)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user