Warum Datumsarithmetik schwieriger ist als gedacht
Auf den ersten Blick wirkt Datumsrechnung trivial: Tage zählen, Monate addieren, fertig. In der Praxis scheitern selbst erfahrene Entwickler an Grenzfällen. Der Grund: Unser Kalendersystem ist keine gleichmäßige mathematische Struktur — es ist ein historisch gewachsenes System mit unregelmäßigen Monatslängen, Schaltjahren, Zeitzonen und kulturspezifischen Feiertagen.
Ein einfaches Beispiel: Was ist "ein Monat nach dem 31. Januar"? Der 31. Februar existiert nicht. Ist es der 28. Februar? Der 1. März? Verschiedene Programmiersprachen und Bibliotheken geben unterschiedliche Antworten. JavaScript gibt den 2. oder 3. März zurück (je nach Schaltjahr), weil es intern überläuft. Python mit dateutil gibt den 28. Februar. Es gibt keine "richtige" Antwort — nur unterschiedliche Konventionen.
Weitere Fallstricke: "Nächsten Dienstag" kann je nach aktuellem Wochentag und Konvention 2 bis 8 Tage in der Zukunft liegen. "In 24 Stunden" ist nicht immer "morgen um die gleiche Uhrzeit" — bei Zeitumstellung kann ein Tag 23 oder 25 Stunden haben. Und "vor genau einem Jahr" ist am 29. Februar besonders interessant.
Diese Komplexität erklärt, warum Datumsbibliotheken so umfangreich sind und warum eigene Implementierungen fast immer Bugs enthalten. In dieser Anleitung behandeln wir die wichtigsten Regeln, Algorithmen und Fallstricke — und warum du für produktiven Code immer eine bewährte Bibliothek verwenden solltest.
Schaltjahre: die vollständigen Regeln
Die Grundregel kennt jeder: Ein Jahr ist ein Schaltjahr, wenn es durch 4 teilbar ist. Aber die vollständige Regel des gregorianischen Kalenders hat drei Stufen: (1) Durch 4 teilbar → Schaltjahr. (2) Durch 100 teilbar → kein Schaltjahr. (3) Durch 400 teilbar → doch Schaltjahr. Deshalb war 1900 kein Schaltjahr, 2000 aber schon.
In der Praxis betrifft die 100er/400er-Regel kaum jemanden im Alltag — der nächste relevante Fall ist 2100. Aber in Software, die mit historischen Daten oder weit in die Zukunft rechnet, ist sie entscheidend. Ein bekanntes Beispiel: Microsoft Excel behandelt 1900 fälschlicherweise als Schaltjahr (ein Kompatibilitätsfehler aus Lotus 1-2-3), was bei Datumsberechnungen vor 1. März 1900 zu einem Tag Abweichung führt.
Für deutsche Feiertage ist der Schaltjahres-Effekt besonders relevant bei Ostern. Der Ostersonntag wird über den Computus (Gaußsche Osterformel) berechnet und kann zwischen dem 22. März und dem 25. April liegen. Da viele deutsche Feiertage relativ zu Ostern definiert sind (Karfreitag, Ostermontag, Christi Himmelfahrt, Pfingstmontag, Fronleichnam), verschieben sie sich jährlich.
Ein praktischer Tipp: Verwende niemals eine eigene Schaltjahr-Prüfung in produktivem Code, wenn deine Programmiersprache eine eingebaute Funktion bietet. In JavaScript ist new Date(year, 1, 29).getDate() === 29 ein funktionierender Trick — aber date-fns.isLeapYear(year) oder die native Temporal-API (ab 2024/25) sind klarer und weniger fehleranfällig.
// Schaltjahr-Prüfung: vollständige gregorianische Regel
function istSchaltjahr(jahr) {
// Regel 1: Durch 4 teilbar → Kandidat
// Regel 2: Durch 100 teilbar → kein Schaltjahr
// Regel 3: Durch 400 teilbar → doch Schaltjahr
return (jahr % 4 === 0 && jahr % 100 !== 0) || (jahr % 400 === 0);
}
// Beispiele
console.log(istSchaltjahr(2024)); // true (durch 4, nicht durch 100)
console.log(istSchaltjahr(1900)); // false (durch 100, nicht durch 400)
console.log(istSchaltjahr(2000)); // true (durch 400)
console.log(istSchaltjahr(2100)); // false (durch 100, nicht durch 400)
// Tage im Monat unter Berücksichtigung von Schaltjahren
function tageImMonat(jahr, monat) {
// monat: 1-12
const tage = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if (monat === 2 && istSchaltjahr(jahr)) return 29;
return tage[monat - 1];
}
console.log(tageImMonat(2024, 2)); // 29
console.log(tageImMonat(2023, 2)); // 28Arbeitstage berechnen: Feiertage berücksichtigen
Die Berechnung von Arbeitstagen ist eine der häufigsten Anforderungen in Geschäftsanwendungen — Liefertermine, Projektplanung, Fristberechnung, Urlaubstage. Die Grundlogik ist simpel: Zähle Tage, überspringe Wochenenden. Aber in Deutschland gibt es 9 bundesweite Feiertage und je nach Bundesland 2-4 zusätzliche Feiertage, die ebenfalls keine Arbeitstage sind.
Die bundeseinheitlichen Feiertage sind: Neujahr (1.1.), Karfreitag (variabel), Ostermontag (variabel), Tag der Arbeit (1.5.), Christi Himmelfahrt (variabel, 39 Tage nach Ostern), Pfingstmontag (variabel, 50 Tage nach Ostern), Tag der Deutschen Einheit (3.10.), 1. Weihnachtsfeiertag (25.12.), 2. Weihnachtsfeiertag (26.12.). Dazu kommen landesspezifische: Heilige Drei Könige (6.1.) in BW/BY/ST, Fronleichnam in BW/BY/HE/NW/RP/SL, Reformationstag (31.10.) in östlichen Bundesländern, Allerheiligen (1.11.) in BW/BY/NW/RP/SL.
Ein zuverlässiger Arbeitstage-Rechner muss daher: (1) variable Feiertage korrekt berechnen (Osterdatum!), (2) das richtige Bundesland berücksichtigen, (3) Brückentage optional einbeziehen, (4) Betriebsurlaub oder Sonderregelungen erlauben. Für Projektplanung ist es außerdem hilfreich, halbe Tage (Heiligabend, Silvester) zu berücksichtigen, die in vielen Tarifverträgen arbeitsfrei sind.
Ein typischer Fehler: Den 3. Oktober als "ersten Montag im Oktober" zu implementieren statt als festes Datum. Ein anderer: Fronleichnam falsch berechnen (es ist der 60. Tag nach Ostersonntag, also 11 Tage nach Pfingsten). Teste deinen Arbeitstage-Algorithmus immer gegen bekannte Jahre und verifiziere Grenzfälle wie den Jahreswechsel und Jahre, in denen ein Feiertag auf ein Wochenende fällt.
// Arbeitstage zwischen zwei Daten berechnen (vereinfacht)
function arbeitstageBerechnen(startDatum, endDatum, feiertage = []) {
let arbeitstage = 0;
const aktuell = new Date(startDatum);
const ende = new Date(endDatum);
while (aktuell <= ende) {
const wochentag = aktuell.getDay();
const datumStr = aktuell.toISOString().split('T')[0];
// Wochenende überspringen (0 = Sonntag, 6 = Samstag)
const istWochenende = wochentag === 0 || wochentag === 6;
// Feiertag prüfen
const istFeiertag = feiertage.includes(datumStr);
if (!istWochenende && !istFeiertag) {
arbeitstage++;
}
aktuell.setDate(aktuell.getDate() + 1);
}
return arbeitstage;
}
// Beispiel: Arbeitstage im Januar 2025 (NRW)
const feiertagNRW = ['2025-01-01']; // Neujahr
const tage = arbeitstageBerechnen('2025-01-01', '2025-01-31', feiertagNRW);
console.log(`Arbeitstage Januar 2025 (NRW): ${tage}`);
// Arbeitstage Januar 2025 (NRW): 22Altersberechnung: scheinbar einfach, überraschend komplex
Wie alt ist jemand, der am 29. Februar 2000 geboren wurde, am 28. Februar 2025? Ist die Person 24 oder 25? In Deutschland gilt rechtlich: Ein am 29. Februar Geborener wird in Nicht-Schaltjahren am 1. März ein Jahr älter (§ 187 Abs. 2 BGB in Verbindung mit § 188 BGB). Aber viele Software-Implementierungen geben den 28. Februar als "Geburtstag" zurück.
Die allgemeine Altersberechnung folgt dieser Logik: Ziehe das Geburtsjahr vom aktuellen Jahr ab. Wenn der Geburtstag in diesem Jahr noch nicht stattgefunden hat, ziehe 1 ab. Der "Geburtstag hat stattgefunden"-Check vergleicht Monat und Tag — und genau hier lauert der Schaltjahr-Sonderfall.
Für viele Anwendungen (Versicherungen, Rentenberechnung, Altersprüfungen) ist das exakte Alter in Jahren, Monaten und Tagen relevant. Die Berechnung von "Monaten Differenz" ist dabei wieder nicht eindeutig: Ist vom 31. Januar zum 28. Februar "ein voller Monat"? Die meisten Bibliotheken sagen ja, aber manche Business-Regeln fordern "gleicher oder späterer Tag im Folgemonat".
Unser Altersrechner verwendet die in Deutschland übliche Konvention: Das vollendete Lebensjahr zählt ab dem Tag nach dem Geburtstag des Vorjahres. Für den 29. Februar bedeutet das: Am 28. Februar eines Nicht-Schaltjahres bist du noch im alten Lebensjahr, am 1. März beginnt das neue. Teste Altersberechnungen immer mit Grenzfällen: 29. Februar, Jahreswechsel, und Personen mit Geburtstag am aktuellen Tag.
Monatsende-Überlauf: das 31.-Januar-Problem
Das "End-of-Month"-Problem tritt auf, wenn man Monate zu einem Datum addiert, das am Ende eines langen Monats liegt. 31. Januar + 1 Monat = ? Der 31. Februar existiert nicht. Verschiedene Systeme lösen das unterschiedlich: JavaScript/Date "überläuft" zum 2. oder 3. März. Python dateutil und Java geben den 28./29. Februar zurück (letzter gültiger Tag). Andere Bibliotheken werfen einen Fehler.
In geschäftlichen Kontexten (Verträge, Kreditraten, Gehaltszahlungen) ist die Konvention meist: "Am selben Tag des Folgemonats, oder am letzten Tag des Folgemonats, falls dieser kürzer ist." Ein Kredit mit Fälligkeit am 31. eines Monats ist im Februar am 28. (oder 29.) fällig, im April am 30., im Mai wieder am 31. Diese "End-of-Month-Regel" muss explizit implementiert werden.
Ein verwandtes Problem: "Letzter Tag des Monats" als wiederkehrendes Ereignis. Wenn eine Zahlung immer am "letzten Tag des Monats" fällig ist, dann ist das der 31. Januar, 28./29. Februar, 31. März, 30. April usw. Das ist anders als "am 31." — denn "am 31." schlägt in Monaten mit 30 oder weniger Tagen fehl.
Best Practice: Definiere bei jeder Monatsaddition explizit die Überlauf-Strategie. Verwende eine Bibliothek, die diese Strategien als Parameter anbietet (date-fns, Luxon, Temporal). Dokumentiere die gewählte Konvention in deinem Code, besonders bei Finanz- und Vertragsanwendungen, wo ein Tag Unterschied rechtliche Konsequenzen haben kann.
Algorithmen für Datumsdifferenzen
Die Differenz zwischen zwei Daten in Tagen zu berechnen ist mathematisch sauber: Wandle beide Daten in eine fortlaufende Tagesnummer um und subtrahiere. Der Julian Day Number (JDN) Algorithmus macht genau das — er weist jedem Datum seit dem 1. Januar 4713 v. Chr. eine eindeutige Ganzzahl zu. In der Praxis nutzen die meisten Sprachen intern Unix-Timestamps (Sekunden seit 1.1.1970) und dividieren durch 86.400.
Die Differenz in Monaten und Jahren ist dagegen nicht eindeutig definiert, weil Monate unterschiedliche Längen haben. "Vom 15. Januar zum 15. März" sind klar 2 Monate. Aber "vom 31. Januar zum 28. Februar" — ist das ein Monat oder 28 Tage? Die übliche Konvention: Vergleiche zuerst die Jahre (Differenz × 12 Monate), dann die Monate, dann die Resttage. Wenn der Starttag größer als der Zieltag ist, ziehe einen Monat ab und addiere die Tage des Vormonats.
Für Geschäftsanwendungen gibt es standardisierte Methoden zur Tagezählung (Day Count Conventions): ACT/ACT (tatsächliche Tage), 30/360 (jeder Monat hat 30 Tage, das Jahr 360), ACT/365 usw. Diese werden vor allem in der Finanzwelt für Zinsberechnungen verwendet. Ein Kredit mit 30/360-Konvention rechnet vom 1. Februar zum 1. März als 30 Tage, obwohl der Februar nur 28/29 hat.
Unser Datumsdifferenz-Rechner verwendet die ACT/ACT-Methode (tatsächliche Tage) für die Tagesanzeige und die oben beschriebene Kalenderkonvention für Jahre/Monate/Tage. Für spezifische Finanzberechnungen solltest du die vertraglich vereinbarte Day Count Convention prüfen und gegebenenfalls eine spezialisierte Finanzbibliothek verwenden.
// Datumsdifferenz in Jahren, Monaten und Tagen
function datumsDifferenz(startStr, endeStr) {
const start = new Date(startStr);
const ende = new Date(endeStr);
let jahre = ende.getFullYear() - start.getFullYear();
let monate = ende.getMonth() - start.getMonth();
let tage = ende.getDate() - start.getDate();
// Wenn Tage negativ: einen Monat "borgen"
if (tage < 0) {
monate--;
// Tage des Vormonats im Zieljahr ermitteln
const vormonat = new Date(ende.getFullYear(), ende.getMonth(), 0);
tage += vormonat.getDate();
}
// Wenn Monate negativ: ein Jahr "borgen"
if (monate < 0) {
jahre--;
monate += 12;
}
// Gesamttage für einfache Differenz
const gesamtTage = Math.round((ende - start) / (1000 * 60 * 60 * 24));
return { jahre, monate, tage, gesamtTage };
}
// Beispiel: Dauer eines Projekts
const diff = datumsDifferenz('2024-03-15', '2025-07-22');
console.log(`Dauer: ${diff.jahre} Jahre, ${diff.monate} Monate, ${diff.tage} Tage`);
// Dauer: 1 Jahre, 4 Monate, 7 Tage
console.log(`Insgesamt: ${diff.gesamtTage} Tage`);
// Insgesamt: 494 TageBibliotheken und Best Practices
Die wichtigste Regel bei Datumsberechnungen: Verwende eine bewährte Bibliothek statt eigener Implementierungen. In JavaScript sind date-fns (funktional, tree-shakeable, immutable) und Luxon (OOP, gute Internationalisierung) die aktuellen Empfehlungen. Moment.js ist seit 2020 offiziell im Maintenance-Mode und sollte für neue Projekte nicht mehr verwendet werden. Die neue Temporal-API wird nativ in JavaScript verfügbar sein und viele Probleme der alten Date-Klasse lösen.
Für deutsche Projekte besonders relevant: Lokalisierung und Feiertags-Berechnung. Die Bibliothek "date-holidays" liefert Feiertage für alle deutschen Bundesländer (und viele andere Länder). Für Geschäftstagberechnungen bieten date-fns-business-days oder eigene Konfigurationen mit date-fns die nötige Flexibilität. Achte immer darauf, dass deine Feiertags-Datenbank aktuell ist — gelegentlich werden Feiertage per Gesetz geändert.
Speichere Daten intern immer als ISO 8601 (YYYY-MM-DD) oder als UTC-Timestamps. Formatiere erst bei der Anzeige ins lokale Format (DD.MM.YYYY in Deutschland). Vermeide das amerikanische MM/DD/YYYY-Format in internationalen Anwendungen — es ist die häufigste Ursache für Datums-Verwechslungen. Und speichere Zeitzonen immer als IANA-Identifier (Europe/Berlin) statt als Offset (+02:00), da der Offset sich durch Sommer-/Winterzeit ändert.
Teste Datumslogik immer mit Grenzfällen: Schaltjahre (29. Februar), Jahreswechsel (31. Dezember → 1. Januar), Monatswechsel mit unterschiedlichen Längen (28/29/30/31), Zeitumstellung (letzter Sonntag im März/Oktober), und kulturspezifische Besonderheiten. Unser Datumsdifferenz-Rechner und Altersrechner verwenden diese Best Practices und behandeln alle beschriebenen Grenzfälle korrekt.