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
@@ -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}`
|
||||
}
|
||||
Reference in new issue
Block a user