130 lines
4.3 KiB
TypeScript
130 lines
4.3 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|