PHP: Wie berechnet man die Tage zwischen zwei Daten?

Die Antwort ist eine Zeile. Interessant ist die andere, ältere Lösung, die man mindestens genauso oft findet: Differenz der Timestamps, geteilt durch 86400. Die ist zweimal im Jahr falsch, und kein Runden repariert das.

Die Antwort

<?php
  $von = new DateTimeImmutable('2026-01-01');
  $bis = new DateTimeImmutable('2026-03-15');

  $tage = $von->diff($bis)->days;
?>
->days : 73

Das war es schon. Der Rest des Artikels handelt davon, warum es die anderen Wege nicht tun und was man bei diff() selbst noch falsch machen kann.

->days ist nicht ->d

Der häufigste Fehler bei diff(). Das zurückgegebene DateInterval hat beides, und sie bedeuten etwas anderes:

2026-01-01 -> 2026-02-01 : ->days=31   ->d=0   ->m=1 ->y=0
2026-01-01 -> 2027-01-01 : ->days=365  ->d=0   ->m=0 ->y=1
2026-01-01 -> 2026-01-10 : ->days=9    ->d=9   ->m=0 ->y=0

->days ist die Gesamtzahl der Tage. ->d ist der Tagesrest, nachdem volle Jahre und Monate abgezogen wurden. Bei einem Jahr Abstand ist ->d gleich 0.

Tückisch ist die dritte Zeile: Innerhalb eines Monats sind beide gleich. Wer seinen Code mit kurzen Zeiträumen testet, merkt den Fehler also nicht.

Die Richtung steckt in ->invert

rueckwaerts: ->days=73 ->invert=1

->days ist immer positiv, egal in welcher Reihenfolge du die Daten übergibst. Wenn du wissen willst, ob das zweite Datum vor dem ersten liegt, schaust du in ->invert — oder vergleichst die Objekte direkt, DateTime lässt sich mit < und > vergleichen.

Jetzt die Rechnung mit den Timestamps

Die alte Variante sieht so aus:

<?php
  $tage = ($ende - $start) / 86400;
?>

86400 Sekunden sind ein Tag. Nur haben nicht alle Tage 86400 Sekunden.

Ich habe das für Wien durchgerechnet, einmal über die Umstellung im Frühjahr und einmal über die im Herbst.

Frühjahr, die Nacht auf den 29. März — eine Stunde fällt weg:

2026-03-28 00:00:00 (CET)
2026-03-30 00:00:00 (CEST)
Differenz in Sekunden : 169200
geteilt durch 86400   : 1.9583333333333
floor() davon         : 1   <- ein Tag zu wenig!
diff()->days          : 2   <- richtig

Zwei Kalendertage, aber nur 47 Stunden. floor() macht daraus einen Tag.

Jetzt könnte man auf die Idee kommen, stattdessen ceil() zu nehmen. Das rettet diesen Fall — und zerstört den anderen.

Herbst, die Nacht auf den 25. Oktober — eine Stunde doppelt:

Differenz in Sekunden : 176400
geteilt durch 86400   : 2.0416666666667   <- mehr als 2
ceil() davon          : 3                 <- ein Tag zu viel!
diff()->days          : 2

Damit steht es fest: Weder floor() noch ceil() funktioniert. Was den einen Fall repariert, macht den anderen kaputt.

                 / 86400    floor()   ceil()   diff()->days
Frühjahr         1.9583     1  ✗      2  ✓     2
Herbst           2.0416     2  ✓      3  ✗     2

diff() rechnet richtig, weil es mit Kalenderdaten arbeitet und nicht mit Sekunden. Es zählt Tage, keine 86400er-Blöcke.

Das ist übrigens der Grund, warum Mietberechnungen, Kündigungsfristen und Urlaubstage-Zähler zweimal im Jahr um einen Tag danebenliegen. Der Fehler wird gemeldet, jemand schaut sich den Code an, findet nichts Auffälliges — und in einer Woche lässt er sich nicht mehr reproduzieren.

Der zweite häufige Fehler: die Uhrzeit

Auch mit diff() kann man danebenliegen, und zwar hier:

01.01. 23:00 -> 02.01. 01:00 : ->days=0

Zwei verschiedene Kalendertage, aber nur zwei Stunden dazwischen. diff() zählt volle 24-Stunden-Abschnitte, und davon ist keiner vergangen.

Meistens will man aber Kalendertage zählen. Dafür setzt du beide Seiten auf Mitternacht:

<?php
  $a = $von->setTime(0, 0);
  $b = $bis->setTime(0, 0);
  $tage = $a->diff($b)->days;
?>
nach setTime(0,0) : ->days=1

Das ist ein eigener Fehler, unabhängig von der Sommerzeit — und er tritt jeden Tag auf, nicht nur zweimal im Jahr. Wenn du Daten aus einer Datenbank liest, in der auch eine Uhrzeit steht, ist er praktisch garantiert.

Schaltjahre

Kurz, weil man sich sonst wundert: Die kommen ohne Zutun richtig heraus.

2024-02-01 -> 2024-03-01 : 29 Tage (Schaltjahr)
2026-02-01 -> 2026-03-01 : 28 Tage

Ungültige Eingaben

Zum Schluss noch ein Unterschied, der einem viel Sucherei ersparen kann:

new DateTimeImmutable('kein datum')
-> DateMalformedStringException: Failed to parse time string (kein datum)

strtotime('kein datum')
-> false

DateTimeImmutable wirft eine Exception, du merkst den Fehler also sofort. strtotime() liefert stumm false — und false ist in einer Rechnung 0, also der 1. Januar 1970. Daher kommen die gelegentlichen Anzeigen von “vor 56 Jahren”.

Zusammenfassung

  • $a->diff($b)->days ist die Antwort.
  • ->d ist nicht die Anzahl der Tage, sondern der Tagesrest.
  • ->days ist immer positiv, die Richtung steht in ->invert.
  • Die Rechnung (b - a) / 86400 ist bei Zeitumstellungen falsch, und weder floor() noch ceil() repariert beide Fälle.
  • Für Kalendertage beide Seiten mit setTime(0, 0) auf Mitternacht setzen.
  • strtotime() scheitert stumm, DateTimeImmutable wirft eine Exception.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).