/* * Datumsrechnung ohne Zeitzonenfallen: alles laeuft ueber 'YYYY-MM-DD' und * UTC-Mitternacht, damit ein Tag nie durch die Sommerzeit verrutscht. */ export function parseDate(value) { if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null; const date = new Date(`${value}T00:00:00Z`); return Number.isNaN(date.getTime()) || iso(date) !== value ? null : date; } export function iso(date) { return date.toISOString().slice(0, 10); } export function today() { const now = new Date(); // Lokales Datum des Servers (Europe/Berlin), nicht UTC. const local = new Date(now.getTime() - now.getTimezoneOffset() * 60000); return local.toISOString().slice(0, 10); } export function addDays(value, days) { const date = parseDate(value); date.setUTCDate(date.getUTCDate() + days); return iso(date); } export function daysBetween(from, to) { return Math.round((parseDate(to) - parseDate(from)) / 86400000); } /** Arbeitstage Mo-Fr im geschlossenen Intervall. */ export function workdays(from, to) { let count = 0; const end = parseDate(to); for (const d = parseDate(from); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { const day = d.getUTCDay(); if (day !== 0 && day !== 6) count++; } return count; } export function monthRange(month) { if (!/^\d{4}-\d{2}$/.test(month || '')) return null; const [y, m] = month.split('-').map(Number); if (m < 1 || m > 12) return null; const last = new Date(Date.UTC(y, m, 0)).getUTCDate(); return { from: `${month}-01`, to: `${month}-${String(last).padStart(2, '0')}` }; } export function isoWeek(value) { const date = parseDate(value); const day = date.getUTCDay() || 7; date.setUTCDate(date.getUTCDate() + 4 - day); const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); return { year: date.getUTCFullYear(), week: Math.ceil(((date - yearStart) / 86400000 + 1) / 7) }; } export function mondayOf(value) { const date = parseDate(value); const day = date.getUTCDay() || 7; date.setUTCDate(date.getUTCDate() - day + 1); return iso(date); } export const MONTHS_DE = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; export function formatDateDe(value) { const [y, m, d] = String(value).slice(0, 10).split('-'); return `${d}.${m}.${y}`; } /** Zeitraum, in dem `value` liegt: day | week | month | quarter | year. */ export function periodRange(granularity, value) { const [y, m] = value.split('-').map(Number); switch (granularity) { case 'day': return { from: value, to: value }; case 'week': { const from = mondayOf(value); return { from, to: addDays(from, 6) }; } case 'month': return monthRange(value.slice(0, 7)); case 'quarter': { const q = Math.floor((m - 1) / 3); const startMonth = String(q * 3 + 1).padStart(2, '0'); const end = monthRange(`${y}-${String(q * 3 + 3).padStart(2, '0')}`); return { from: `${y}-${startMonth}-01`, to: end.to }; } case 'year': return { from: `${y}-01-01`, to: `${y}-12-31` }; default: return null; } } /** Beginn des vorherigen Zeitraums (fuer Reihen und Vergleich). */ export function previousStart(granularity, from) { const d = parseDate(from); if (granularity === 'day') d.setUTCDate(d.getUTCDate() - 1); if (granularity === 'week') d.setUTCDate(d.getUTCDate() - 7); if (granularity === 'month') d.setUTCMonth(d.getUTCMonth() - 1); if (granularity === 'quarter') d.setUTCMonth(d.getUTCMonth() - 3); if (granularity === 'year') d.setUTCFullYear(d.getUTCFullYear() - 1); return iso(d); } export function periodLabel(granularity, from) { const [y, m, d] = from.split('-'); if (granularity === 'day') return `${d}.${m}.`; if (granularity === 'week') return `KW ${isoWeek(from).week}`; if (granularity === 'month') return `${MONTHS_DE[Number(m) - 1].slice(0, 3)} ${y.slice(2)}`; if (granularity === 'quarter') return `Q${Math.floor((Number(m) - 1) / 3) + 1} ${y.slice(2)}`; return y; } /** Ostersonntag (Gauss/Meeus, gregorianisch) als 'YYYY-MM-DD'. */ export function easterSunday(year) { const a = year % 19; const b = Math.floor(year / 100); const c = year % 100; const d = Math.floor(b / 4); const e = b % 4; const f = Math.floor((b + 8) / 25); const g = Math.floor((b - f + 1) / 3); const h = (19 * a + b - d - g + 15) % 30; const i = Math.floor(c / 4); const k = c % 4; const l = (32 + 2 * e + 2 * i - h - k) % 7; const m = Math.floor((a + 11 * h + 22 * l) / 451); const month = Math.floor((h + l - 7 * m + 114) / 31); const day = ((h + l - 7 * m + 114) % 31) + 1; return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; } /** * Name eines deutschen Feiertags zum Datum. HRworks sagt nur, DASS ein Tag * Feiertag ist (welcher, haengt am Standort der Person); der Name kommt von * hier. Unbekannt: null. */ export function germanHolidayName(value) { const year = Number(value.slice(0, 4)); const fixed = { '01-01': 'Neujahr', '01-06': 'Heilige Drei Könige', '03-08': 'Frauentag', '05-01': 'Tag der Arbeit', '08-08': 'Friedensfest', '08-15': 'Mariä Himmelfahrt', '09-20': 'Weltkindertag', '10-03': 'Tag der Deutschen Einheit', '10-31': 'Reformationstag', '11-01': 'Allerheiligen', '12-24': 'Heiligabend', '12-25': '1. Weihnachtstag', '12-26': '2. Weihnachtstag', '12-31': 'Silvester', }; if (fixed[value.slice(5)]) return fixed[value.slice(5)]; const easter = easterSunday(year); const offset = daysBetween(easter, value); const moving = { '-2': 'Karfreitag', 0: 'Ostersonntag', 1: 'Ostermontag', 39: 'Christi Himmelfahrt', 49: 'Pfingstsonntag', 50: 'Pfingstmontag', 60: 'Fronleichnam', }; if (moving[offset]) return moving[offset]; // Buss- und Bettag: Mittwoch vor dem 23. November. const nov23 = parseDate(`${year}-11-23`); const back = ((nov23.getUTCDay() - 3 + 7) % 7) || 7; if (value === addDays(`${year}-11-23`, -back)) return 'Buß- und Bettag'; return null; }