Files
everythingbutwork/apps/api/src/lib/holidays.ts
T
dejanandClaude Opus 4.8 bcae44eeb3
ci / quality (push) Failing after 38s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 55s
fix(streak): Feiertage, Wochenend-Edge & Lookback-Deckel
- Feiertage: isWerktag berücksichtigt jetzt die 9 bundesweiten gesetzlichen
  Feiertage (Oster-Computus + feste Tage) → ein Feiertag bricht die Streak
  nicht mehr (zählt wie ein Wochenende neutral). Landesspezifische Feiertage
  bewusst (noch) nicht enthalten.
- Wochenend-Edge: Reflexion an Sa/So setzte die angezeigte Streak auf 0
  (Schleife brach am Nicht-Werktag ab). Jetzt wird übersprungen statt gebrochen.
- Lookback von 120 auf 400 Tage erhöht, damit lange Streaks nicht
  abgeschnitten werden.
- Streak-Zähllogik in reine, getestete countStreakDays() ausgelagert
  (8 Streak- + 5 Feiertags-Tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:42:25 +02:00

61 lines
2.3 KiB
TypeScript

// Bundesweite gesetzliche Feiertage in Deutschland. Diese 9 gelten in ALLEN
// Bundesländern — sie als Nicht-Werktag zu behandeln ist daher immer korrekt.
// Landesspezifische Feiertage (z. B. Fronleichnam, Reformationstag) sind hier
// bewusst NICHT enthalten; die können bei Bedarf pro Bundesland ergänzt werden.
const cache = new Map<number, Set<string>>();
function isoFromParts(year: number, month: number, day: number): string {
return new Date(Date.UTC(year, month - 1, day)).toISOString().slice(0, 10);
}
function addDays(year: number, month: number, day: number, offset: number): string {
const d = new Date(Date.UTC(year, month - 1, day));
d.setUTCDate(d.getUTCDate() + offset);
return d.toISOString().slice(0, 10);
}
// Ostersonntag (gregorianisch) nach Meeus/Jones/Butcher.
function easterSunday(year: number): { month: number; day: number } {
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); // 3 = März, 4 = April
const day = ((h + l - 7 * m + 114) % 31) + 1;
return { month, day };
}
export function germanNationwideHolidays(year: number): Set<string> {
const cached = cache.get(year);
if (cached) return cached;
const e = easterSunday(year);
const set = new Set<string>([
isoFromParts(year, 1, 1), // Neujahr
addDays(year, e.month, e.day, -2), // Karfreitag
addDays(year, e.month, e.day, 1), // Ostermontag
isoFromParts(year, 5, 1), // Tag der Arbeit
addDays(year, e.month, e.day, 39), // Christi Himmelfahrt
addDays(year, e.month, e.day, 50), // Pfingstmontag
isoFromParts(year, 10, 3), // Tag der Deutschen Einheit
isoFromParts(year, 12, 25), // 1. Weihnachtstag
isoFromParts(year, 12, 26), // 2. Weihnachtstag
]);
cache.set(year, set);
return set;
}
export function isGermanHoliday(iso: string): boolean {
const year = Number(iso.slice(0, 4));
if (!Number.isFinite(year)) return false;
return germanNationwideHolidays(year).has(iso);
}