/** * German public holiday calculator. * Supports federal holidays + Bavaria (BY) specific holidays. * * Easter-based dates use the Gauss/Meeus algorithm. */ export interface PublicHoliday { date: string; // ISO "YYYY-MM-DD" name: string; federal: boolean; // true = all states; false = state-specific states?: string[]; // which state abbreviations observe this holiday } /** * Compute Easter Sunday date for a given year (Gregorian calendar). * Uses the Anonymous Gregorian algorithm. */ function computeEaster(year: number): Date { 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); // 1-based const day = ((h + l - 7 * m + 114) % 31) + 1; return new Date(Date.UTC(year, month - 1, day)); } function addDays(date: Date, days: number): Date { const d = new Date(date); d.setUTCDate(d.getUTCDate() + days); return d; } function fmt(date: Date): string { return date.toISOString().slice(0, 10); } function fixed(year: number, month: number, day: number): string { return fmt(new Date(Date.UTC(year, month - 1, day))); } /** * Return all public holidays for a given year and optional state. * When state is omitted, returns federal holidays only. * When state is provided (e.g. "BY"), returns federal + state-specific holidays. */ export function getPublicHolidays(year: number, state?: string): PublicHoliday[] { const easter = computeEaster(year); const holidays: PublicHoliday[] = [ // Federal holidays (all states) { date: fixed(year, 1, 1), name: "Neujahr", federal: true }, { date: fixed(year, 5, 1), name: "Tag der Arbeit", federal: true }, { date: fixed(year, 10, 3), name: "Tag der Deutschen Einheit", federal: true }, { date: fixed(year, 12, 25), name: "1. Weihnachtstag", federal: true }, { date: fixed(year, 12, 26), name: "2. Weihnachtstag", federal: true }, // Easter-based federal holidays { date: fmt(addDays(easter, -2)), name: "Karfreitag", federal: true }, { date: fmt(easter), name: "Ostersonntag", federal: true }, { date: fmt(addDays(easter, 1)), name: "Ostermontag", federal: true }, { date: fmt(addDays(easter, 39)), name: "Christi Himmelfahrt", federal: true }, { date: fmt(addDays(easter, 49)), name: "Pfingstsonntag", federal: true }, { date: fmt(addDays(easter, 50)), name: "Pfingstmontag", federal: true }, // Bavaria-specific (BY) { date: fixed(year, 1, 6), name: "Heilige Drei Könige", federal: false, states: ["BY", "BW", "ST"], }, { date: fmt(addDays(easter, 60)), name: "Fronleichnam", federal: false, states: ["BY", "BW", "HE", "NW", "RP", "SL"], }, { date: fixed(year, 8, 15), name: "Mariä Himmelfahrt", federal: false, states: ["BY", "SL"], }, { date: fixed(year, 11, 1), name: "Allerheiligen", federal: false, states: ["BY", "BW", "NW", "RP", "SL"], }, // Other state-specific (not BY but included for completeness) { date: fixed(year, 10, 31), name: "Reformationstag", federal: false, states: ["BB", "HB", "HH", "MV", "NI", "SH", "SN", "ST", "TH"], }, { date: fixed(year, 11, 18), name: "Buß- und Bettag", federal: false, states: ["SN"], }, ]; if (!state) { return holidays.filter((h) => h.federal); } return holidays.filter((h) => h.federal || h.states?.includes(state)); } /** * Check if a given date (ISO string or Date) is a public holiday. */ export function isPublicHoliday(date: Date | string, state?: string): boolean { const d = typeof date === "string" ? date : date.toISOString().slice(0, 10); const year = parseInt(d.slice(0, 4), 10); return getPublicHolidays(year, state).some((h) => h.date === d); }