PHP: How do you calculate the days between two dates?

The answer is one line. What is interesting is the other, older solution you find at least as often: the difference of the timestamps, divided by 86400. That one is wrong twice a year, and no amount of rounding fixes it.

The answer

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

  $days = $from->diff($to)->days;
?>
->days : 73

That is all. The rest of this article is about why the other routes do not do it, and what you can still get wrong with diff() itself.

->days is not ->d

The most common mistake with diff(). The returned DateInterval has both, and they mean different things:

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 is the total number of days. ->d is the remainder in days after full years and months have been subtracted. At a year’s distance ->d equals 0.

The third line is the treacherous one: within a single month the two are equal. So anyone testing their code with short spans does not notice the bug.

The direction is in ->invert

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

->days is always positive, no matter which order you pass the dates in. If you want to know whether the second date lies before the first, look at ->invert — or compare the objects directly, DateTime can be compared with < and >.

Now the timestamp calculation

The old variant looks like this:

<?php
  $days = ($end - $start) / 86400;
?>

86400 seconds is one day. Except not every day has 86400 seconds.

I worked this through for Vienna, once across the spring changeover and once across the autumn one.

Spring, the night of 29 March — one hour disappears:

2026-03-28 00:00:00 (CET)
2026-03-30 00:00:00 (CEST)
difference in seconds : 169200
divided by 86400      : 1.9583333333333
floor() of that       : 1   <- one day too few!
diff()->days          : 2   <- correct

Two calendar days, but only 47 hours. floor() turns that into one day.

At this point you might think of using ceil() instead. That rescues this case — and destroys the other one.

Autumn, the night of 25 October — one hour twice over:

difference in seconds : 176400
divided by 86400      : 2.0416666666667   <- more than 2
ceil() of that        : 3                 <- one day too many!
diff()->days          : 2

So that settles it: neither floor() nor ceil() works. Whatever fixes the one case breaks the other.

                 / 86400    floor()   ceil()   diff()->days
spring           1.9583     1  ✗      2  ✓     2
autumn           2.0416     2  ✓      3  ✗     2

diff() gets it right because it works with calendar dates and not with seconds. It counts days, not blocks of 86400.

This, incidentally, is why rent calculations, notice periods and holiday-day counters are off by one twice a year. The bug gets reported, somebody looks at the code, finds nothing suspicious — and a week later it can no longer be reproduced.

The second common mistake: the time of day

You can go wrong with diff() too, namely here:

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

Two different calendar days, but only two hours between them. diff() counts full 24-hour stretches, and not one of those has elapsed.

Usually, though, you want to count calendar days. For that you set both sides to midnight:

<?php
  $a = $from->setTime(0, 0);
  $b = $to->setTime(0, 0);
  $days = $a->diff($b)->days;
?>
after setTime(0,0) : ->days=1

This is a separate bug, independent of daylight saving — and it happens every day, not just twice a year. If you read dates from a database that also stores a time, it is practically guaranteed.

Leap years

Briefly, since otherwise you wonder: they come out right without any help.

2024-02-01 -> 2024-03-01 : 29 days (leap year)
2026-02-01 -> 2026-03-01 : 28 days

Invalid input

Finally a difference that can save you a lot of searching:

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

strtotime('kein datum')
-> false

DateTimeImmutable throws an exception, so you notice the problem straight away. strtotime() silently returns false — and false in a calculation is 0, so 1 January 1970. That is where the occasional “56 years ago” displays come from.

Summary

  • $a->diff($b)->days is the answer.
  • ->d is not the number of days, it is the remainder in days.
  • ->days is always positive, the direction is in ->invert.
  • The calculation (b - a) / 86400 is wrong across daylight saving changes, and neither floor() nor ceil() fixes both cases.
  • For calendar days, set both sides to midnight with setTime(0, 0).
  • strtotime() fails silently, DateTimeImmutable throws an exception.

About Netcup (advertisement)

The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).