Dates7 min readExamples run on PHP 8.5.10

date_diff()

Learn how PHP 8.4 date_diff() compares two date objects and returns a DateInterval, including invert, absolute mode, total days and time zone behavior.

Signature

date_diff(DateTimeInterface $baseObject, DateTimeInterface $targetObject, bool $absolute = false): DateInterval
Available since PHP 5.3.07.48.08.18.28.38.48.5All versions →

Purpose

date_diff() measures the gap between two points in time. You give it a starting date object and an ending date object, and it returns a DateInterval that describes the gap in years, months, days, hours, minutes, seconds and microseconds. The manual describes it as a procedural alias of DateTime::diff(), so both forms behave the same way. Choose whichever fits your code style: the function in procedural code, the method in object-oriented code.

Signature

Parameters and defaults

  • $baseObject: the starting point. Any DateTime or DateTimeImmutable works, because both implement DateTimeInterface.
  • $targetObject: the point you compare against. The interval runs from the base to the target.
  • $absolute: defaults to false. When true, the interval is always reported as positive and invert is 0, even if the target is earlier than the base.

Return value and failures

The function returns a DateInterval. Its main properties are y, m, d, h, i, s and f, plus invert and days. invert is 1 when the target is earlier than the base (and $absolute is false). days is the total number of whole days between the two dates. It is a different value from d, which is only the leftover day part after whole months and years are taken out.

The function does not return false for a bad comparison. If you pass something that is not a DateTimeInterface, such as a plain string or false from a failed date_create(), PHP throws a TypeError. Validate or construct your objects first.

Example 1: a basic difference

The two dates below are in a leap year. The result is two whole months, five days, six hours and thirty minutes, and 65 full days in total.

Example
<?php
date_default_timezone_set('UTC');
$start = date_create('2024-01-15 08:00:00');
$end = date_create('2024-03-20 14:30:00');
$diff = date_diff($start, $end);
echo json_encode([
    'y' => $diff->y,
    'm' => $diff->m,
    'd' => $diff->d,
    'h' => $diff->h,
    'i' => $diff->i,
    'invert' => $diff->invert,
    'days' => $diff->days,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
Output
{"y":0,"m":2,"d":5,"h":6,"i":30,"invert":0,"days":65}

Notice that d is 5 while days is 65. Use days when you want a total count, and the separate fields when you want a calendar-style breakdown.

Example 2: direction and absolute mode

When the target is earlier than the base, the interval is marked as inverted. Passing true as the third argument removes the sign. The %R format character prints + or - based on invert.

Example
<?php
date_default_timezone_set('UTC');
$a = new DateTimeImmutable('2024-05-10');
$b = new DateTimeImmutable('2024-05-01');
$signed = date_diff($a, $b);
$absolute = date_diff($a, $b, true);
echo json_encode([
    'signedInvert' => $signed->invert,
    'signedDays' => $signed->days,
    'absoluteInvert' => $absolute->invert,
    'absoluteDays' => $absolute->days,
    'format' => $signed->format('%R%a days'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
Output
{"signedInvert":1,"signedDays":9,"absoluteInvert":0,"absoluteDays":9,"format":"-9 days"}

Both intervals report 9 days. Only the sign information differs.

Example 3: different offsets

The two timestamps below have the same wall-clock text but different UTC offsets. They name different instants, and the difference reflects that.

Example
<?php
$base = new DateTimeImmutable('2024-01-01 00:00:00+00:00');
$target = new DateTimeImmutable('2024-01-01 00:00:00-05:00');
$diff = date_diff($base, $target);
echo json_encode([
    'hours' => $diff->h,
    'days' => $diff->days,
    'text' => $diff->format('%R%H:%I'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
Output
{"hours":5,"days":0,"text":"+05:00"}

The target is five hours later in absolute time, so the result is a positive five-hour interval and zero full days.

Edge cases

  • Argument order matters. Swapping base and target flips invert but keeps the magnitude the same.
  • days versus d. Do not use d as a total day count. A gap of two months and five days has d equal to 5.
  • Month lengths vary. Month and day parts depend on the calendar dates involved, so do not convert m into a fixed number of days yourself.
  • Time zones. Objects with different zones are compared as instants. If you want to compare local calendar dates, create both objects in the same zone first.
  • Immutability. The function does not modify either input object.
  • Hand-built intervals. An interval you create yourself with new DateInterval() does not have a computed total-day value, so days is only meaningful on results returned by a diff.

Common mistakes

  1. Reading $diff->d and treating it as the full number of days between two dates.
  2. Forgetting invert and showing a negative gap as positive, or the reverse.
  3. Passing the result of a failed date_create() directly to date_diff(), which causes a TypeError because false is not a date object.
  4. Comparing a date-only value with a date-and-time value and being surprised by leftover hours. A date string without a time is treated as midnight.
  5. Mixing time zones unintentionally, for example by building one object from a string with an offset and another from the default zone.
  • DateTime::diff(): the method form of the same operation.
  • date_create(): builds a DateTime from a string.
  • date_add() and date_sub(): apply an interval to a date.
  • DateInterval::format(): render an interval as text.
  • date_default_timezone_set(): set the default zone for parsing strings without an offset.

Recap

date_diff() takes a base and a target date object and returns a DateInterval. Use days for the total day count, invert or %R for direction, and the third argument when you only care about magnitude. Keep time zones consistent, and make sure both arguments are real date objects.

Tested examples

Basic difference between two datetimes
<?php
date_default_timezone_set('UTC');
$start = date_create('2024-01-15 08:00:00');
$end = date_create('2024-03-20 14:30:00');
$diff = date_diff($start, $end);
echo json_encode([
    'y' => $diff->y,
    'm' => $diff->m,
    'd' => $diff->d,
    'h' => $diff->h,
    'i' => $diff->i,
    'invert' => $diff->invert,
    'days' => $diff->days,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
Output
{"y":0,"m":2,"d":5,"h":6,"i":30,"invert":0,"days":65}
Inverted and absolute intervals
<?php
date_default_timezone_set('UTC');
$a = new DateTimeImmutable('2024-05-10');
$b = new DateTimeImmutable('2024-05-01');
$signed = date_diff($a, $b);
$absolute = date_diff($a, $b, true);
echo json_encode([
    'signedInvert' => $signed->invert,
    'signedDays' => $signed->days,
    'absoluteInvert' => $absolute->invert,
    'absoluteDays' => $absolute->days,
    'format' => $signed->format('%R%a days'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
Output
{"signedInvert":1,"signedDays":9,"absoluteInvert":0,"absoluteDays":9,"format":"-9 days"}
Comparing instants with different UTC offsets
<?php
$base = new DateTimeImmutable('2024-01-01 00:00:00+00:00');
$target = new DateTimeImmutable('2024-01-01 00:00:00-05:00');
$diff = date_diff($base, $target);
echo json_encode([
    'hours' => $diff->h,
    'days' => $diff->days,
    'text' => $diff->format('%R%H:%I'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
Output
{"hours":5,"days":0,"text":"+05:00"}