Categories
datediff datetime php

How to calculate the difference between two dates using PHP?

785

I have two dates of the form:

Start Date: 2007-03-24 
End Date: 2009-06-26

Now I need to find the difference between these two in the following form:

2 years, 3 months and 2 days

How can I do this in PHP?

2

  • 2

    2 years 94 days. Calculating the months, taking into account leap years, would be problematic. How accurate does this need to be?

    – dbasnett

    Mar 24, 2009 at 12:35

  • possible duplicate of How do I calculate relative time?

    Jun 26, 2014 at 5:00

577

Use this for legacy code (PHP < 5.3). For up to date solution see jurka’s answer below

You can use strtotime() to convert two dates to unix time and then calculate the number of seconds between them. From this it’s rather easy to calculate different time periods.

$date1 = "2007-03-24";
$date2 = "2009-06-26";

$diff = abs(strtotime($date2) - strtotime($date1));

$years = floor($diff / (365*60*60*24));
$months = floor(($diff - $years * 365*60*60*24) / (30*60*60*24));
$days = floor(($diff - $years * 365*60*60*24 - $months*30*60*60*24)/ (60*60*24));

printf("%d years, %d months, %d days\n", $years, $months, $days);

Edit: Obviously the preferred way of doing this is like described by jurka below. My code is generally only recommended if you don’t have PHP 5.3 or better.

Several people in the comments have pointed out that the code above is only an approximation. I still believe that for most purposes that’s fine, since the usage of a range is more to provide a sense of how much time has passed or remains rather than to provide precision – if you want to do that, just output the date.

Despite all that, I’ve decided to address the complaints. If you truly need an exact range but haven’t got access to PHP 5.3, use the code below (it should work in PHP 4 as well). This is a direct port of the code that PHP uses internally to calculate ranges, with the exception that it doesn’t take daylight savings time into account. That means that it’s off by an hour at most, but except for that it should be correct.

<?php

/**
 * Calculate differences between two dates with precise semantics. Based on PHPs DateTime::diff()
 * implementation by Derick Rethans. Ported to PHP by Emil H, 2011-05-02. No rights reserved.
 * 
 * See here for original code:
 * http://svn.php.net/viewvc/php/php-src/trunk/ext/date/lib/tm2unixtime.c?revision=302890&view=markup
 * http://svn.php.net/viewvc/php/php-src/trunk/ext/date/lib/interval.c?revision=298973&view=markup
 */

function _date_range_limit($start, $end, $adj, $a, $b, $result)
{
    if ($result[$a] < $start) {
        $result[$b] -= intval(($start - $result[$a] - 1) / $adj) + 1;
        $result[$a] += $adj * intval(($start - $result[$a] - 1) / $adj + 1);
    }

    if ($result[$a] >= $end) {
        $result[$b] += intval($result[$a] / $adj);
        $result[$a] -= $adj * intval($result[$a] / $adj);
    }

    return $result;
}

function _date_range_limit_days($base, $result)
{
    $days_in_month_leap = array(31, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
    $days_in_month = array(31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);

    _date_range_limit(1, 13, 12, "m", "y", &$base);

    $year = $base["y"];
    $month = $base["m"];

    if (!$result["invert"]) {
        while ($result["d"] < 0) {
            $month--;
            if ($month < 1) {
                $month += 12;
                $year--;
            }

            $leapyear = $year % 400 == 0 || ($year % 100 != 0 && $year % 4 == 0);
            $days = $leapyear ? $days_in_month_leap[$month] : $days_in_month[$month];

            $result["d"] += $days;
            $result["m"]--;
        }
    } else {
        while ($result["d"] < 0) {
            $leapyear = $year % 400 == 0 || ($year % 100 != 0 && $year % 4 == 0);
            $days = $leapyear ? $days_in_month_leap[$month] : $days_in_month[$month];

            $result["d"] += $days;
            $result["m"]--;

            $month++;
            if ($month > 12) {
                $month -= 12;
                $year++;
            }
        }
    }

    return $result;
}

function _date_normalize($base, $result)
{
    $result = _date_range_limit(0, 60, 60, "s", "i", $result);
    $result = _date_range_limit(0, 60, 60, "i", "h", $result);
    $result = _date_range_limit(0, 24, 24, "h", "d", $result);
    $result = _date_range_limit(0, 12, 12, "m", "y", $result);

    $result = _date_range_limit_days(&$base, &$result);

    $result = _date_range_limit(0, 12, 12, "m", "y", $result);

    return $result;
}

/**
 * Accepts two unix timestamps.
 */
function _date_diff($one, $two)
{
    $invert = false;
    if ($one > $two) {
        list($one, $two) = array($two, $one);
        $invert = true;
    }

    $key = array("y", "m", "d", "h", "i", "s");
    $a = array_combine($key, array_map("intval", explode(" ", date("Y m d H i s", $one))));
    $b = array_combine($key, array_map("intval", explode(" ", date("Y m d H i s", $two))));

    $result = array();
    $result["y"] = $b["y"] - $a["y"];
    $result["m"] = $b["m"] - $a["m"];
    $result["d"] = $b["d"] - $a["d"];
    $result["h"] = $b["h"] - $a["h"];
    $result["i"] = $b["i"] - $a["i"];
    $result["s"] = $b["s"] - $a["s"];
    $result["invert"] = $invert ? 1 : 0;
    $result["days"] = intval(abs(($one - $two)/86400));

    if ($invert) {
        _date_normalize(&$a, &$result);
    } else {
        _date_normalize(&$b, &$result);
    }

    return $result;
}

$date = "1986-11-10 19:37:22";

print_r(_date_diff(strtotime($date), time()));
print_r(_date_diff(time(), strtotime($date)));

8

  • 1

    If you’re using the DateTime class you can go for $date->format(‘U’) to get the unix timestamp.

    – Jon Cram

    Aug 7, 2009 at 13:26

  • 4

    It’s not true if you have to deal with summer/winter time. In this particular case when you adjust summer/winter time, one day equals 23 or 25 hours.

    – Arno

    Dec 21, 2009 at 15:54

  • 4

    Well, the same argument could be made for leap years. It doesn’t take that into account either. Still, I’m not convinced that you even want to take that into account since we’re discussing a range here. The semantics for a range are somewhat different than for an absolute date.

    – Emil H

    Dec 21, 2009 at 20:35

  • 9

    This function is incorrect. It’s good for an approximation, but incorrect for exact ranges. For one, it assumes there are 30 days in a month, which is to say it will have the same difference of days between February 1st and March 1st as it will for July 1st to August 1st (regardless of leap year).

    – enobrev

    Apr 11, 2011 at 19:14

  • 1

    In PHP, reference variables are in the function signature, not the call. Move all your & to the signatures.

    Mar 19, 2013 at 9:32

577

Use this for legacy code (PHP < 5.3). For up to date solution see jurka’s answer below

You can use strtotime() to convert two dates to unix time and then calculate the number of seconds between them. From this it’s rather easy to calculate different time periods.

$date1 = "2007-03-24";
$date2 = "2009-06-26";

$diff = abs(strtotime($date2) - strtotime($date1));

$years = floor($diff / (365*60*60*24));
$months = floor(($diff - $years * 365*60*60*24) / (30*60*60*24));
$days = floor(($diff - $years * 365*60*60*24 - $months*30*60*60*24)/ (60*60*24));

printf("%d years, %d months, %d days\n", $years, $months, $days);

Edit: Obviously the preferred way of doing this is like described by jurka below. My code is generally only recommended if you don’t have PHP 5.3 or better.

Several people in the comments have pointed out that the code above is only an approximation. I still believe that for most purposes that’s fine, since the usage of a range is more to provide a sense of how much time has passed or remains rather than to provide precision – if you want to do that, just output the date.

Despite all that, I’ve decided to address the complaints. If you truly need an exact range but haven’t got access to PHP 5.3, use the code below (it should work in PHP 4 as well). This is a direct port of the code that PHP uses internally to calculate ranges, with the exception that it doesn’t take daylight savings time into account. That means that it’s off by an hour at most, but except for that it should be correct.

<?php

/**
 * Calculate differences between two dates with precise semantics. Based on PHPs DateTime::diff()
 * implementation by Derick Rethans. Ported to PHP by Emil H, 2011-05-02. No rights reserved.
 * 
 * See here for original code:
 * http://svn.php.net/viewvc/php/php-src/trunk/ext/date/lib/tm2unixtime.c?revision=302890&view=markup
 * http://svn.php.net/viewvc/php/php-src/trunk/ext/date/lib/interval.c?revision=298973&view=markup
 */

function _date_range_limit($start, $end, $adj, $a, $b, $result)
{
    if ($result[$a] < $start) {
        $result[$b] -= intval(($start - $result[$a] - 1) / $adj) + 1;
        $result[$a] += $adj * intval(($start - $result[$a] - 1) / $adj + 1);
    }

    if ($result[$a] >= $end) {
        $result[$b] += intval($result[$a] / $adj);
        $result[$a] -= $adj * intval($result[$a] / $adj);
    }

    return $result;
}

function _date_range_limit_days($base, $result)
{
    $days_in_month_leap = array(31, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
    $days_in_month = array(31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);

    _date_range_limit(1, 13, 12, "m", "y", &$base);

    $year = $base["y"];
    $month = $base["m"];

    if (!$result["invert"]) {
        while ($result["d"] < 0) {
            $month--;
            if ($month < 1) {
                $month += 12;
                $year--;
            }

            $leapyear = $year % 400 == 0 || ($year % 100 != 0 && $year % 4 == 0);
            $days = $leapyear ? $days_in_month_leap[$month] : $days_in_month[$month];

            $result["d"] += $days;
            $result["m"]--;
        }
    } else {
        while ($result["d"] < 0) {
            $leapyear = $year % 400 == 0 || ($year % 100 != 0 && $year % 4 == 0);
            $days = $leapyear ? $days_in_month_leap[$month] : $days_in_month[$month];

            $result["d"] += $days;
            $result["m"]--;

            $month++;
            if ($month > 12) {
                $month -= 12;
                $year++;
            }
        }
    }

    return $result;
}

function _date_normalize($base, $result)
{
    $result = _date_range_limit(0, 60, 60, "s", "i", $result);
    $result = _date_range_limit(0, 60, 60, "i", "h", $result);
    $result = _date_range_limit(0, 24, 24, "h", "d", $result);
    $result = _date_range_limit(0, 12, 12, "m", "y", $result);

    $result = _date_range_limit_days(&$base, &$result);

    $result = _date_range_limit(0, 12, 12, "m", "y", $result);

    return $result;
}

/**
 * Accepts two unix timestamps.
 */
function _date_diff($one, $two)
{
    $invert = false;
    if ($one > $two) {
        list($one, $two) = array($two, $one);
        $invert = true;
    }

    $key = array("y", "m", "d", "h", "i", "s");
    $a = array_combine($key, array_map("intval", explode(" ", date("Y m d H i s", $one))));
    $b = array_combine($key, array_map("intval", explode(" ", date("Y m d H i s", $two))));

    $result = array();
    $result["y"] = $b["y"] - $a["y"];
    $result["m"] = $b["m"] - $a["m"];
    $result["d"] = $b["d"] - $a["d"];
    $result["h"] = $b["h"] - $a["h"];
    $result["i"] = $b["i"] - $a["i"];
    $result["s"] = $b["s"] - $a["s"];
    $result["invert"] = $invert ? 1 : 0;
    $result["days"] = intval(abs(($one - $two)/86400));

    if ($invert) {
        _date_normalize(&$a, &$result);
    } else {
        _date_normalize(&$b, &$result);
    }

    return $result;
}

$date = "1986-11-10 19:37:22";

print_r(_date_diff(strtotime($date), time()));
print_r(_date_diff(time(), strtotime($date)));

8

  • 1

    If you’re using the DateTime class you can go for $date->format(‘U’) to get the unix timestamp.

    – Jon Cram

    Aug 7, 2009 at 13:26

  • 4

    It’s not true if you have to deal with summer/winter time. In this particular case when you adjust summer/winter time, one day equals 23 or 25 hours.

    – Arno

    Dec 21, 2009 at 15:54

  • 4

    Well, the same argument could be made for leap years. It doesn’t take that into account either. Still, I’m not convinced that you even want to take that into account since we’re discussing a range here. The semantics for a range are somewhat different than for an absolute date.

    – Emil H

    Dec 21, 2009 at 20:35

  • 9

    This function is incorrect. It’s good for an approximation, but incorrect for exact ranges. For one, it assumes there are 30 days in a month, which is to say it will have the same difference of days between February 1st and March 1st as it will for July 1st to August 1st (regardless of leap year).

    – enobrev

    Apr 11, 2011 at 19:14

  • 1

    In PHP, reference variables are in the function signature, not the call. Move all your & to the signatures.

    Mar 19, 2013 at 9:32

82

The best course of action is using PHP’s DateTime (and DateInterval) objects. Each date is encapsulated in a DateTime object, and then a difference between the two can be made:

$first_date = new DateTime("2012-11-30 17:03:30");
$second_date = new DateTime("2012-12-21 00:00:00");

The DateTime object will accept any format strtotime() would. If an even more specific date format is needed, DateTime::createFromFormat() can be used to create the DateTime object.

After both objects were instantiated, you substract one from the other with DateTime::diff().

$difference = $first_date->diff($second_date);

$difference now holds a DateInterval object with the difference information. A var_dump() looks like this:

object(DateInterval)
  public 'y' => int 0
  public 'm' => int 0
  public 'd' => int 20
  public 'h' => int 6
  public 'i' => int 56
  public 's' => int 30
  public 'invert' => int 0
  public 'days' => int 20

To format the DateInterval object, we’ll need check each value and exclude it if it’s 0:

/**
 * Format an interval to show all existing components.
 * If the interval doesn't have a time component (years, months, etc)
 * That component won't be displayed.
 *
 * @param DateInterval $interval The interval
 *
 * @return string Formatted interval string.
 */
function format_interval(DateInterval $interval) {
    $result = "";
    if ($interval->y) { $result .= $interval->format("%y years "); }
    if ($interval->m) { $result .= $interval->format("%m months "); }
    if ($interval->d) { $result .= $interval->format("%d days "); }
    if ($interval->h) { $result .= $interval->format("%h hours "); }
    if ($interval->i) { $result .= $interval->format("%i minutes "); }
    if ($interval->s) { $result .= $interval->format("%s seconds "); }

    return $result;
}

All that’s left now is to call our function on the $difference DateInterval object:

echo format_interval($difference);

And we get the correct result:

20 days 6 hours 56 minutes 30 seconds

The complete code used to achieve the goal:

/**
 * Format an interval to show all existing components.
 * If the interval doesn't have a time component (years, months, etc)
 * That component won't be displayed.
 *
 * @param DateInterval $interval The interval
 *
 * @return string Formatted interval string.
 */
function format_interval(DateInterval $interval) {
    $result = "";
    if ($interval->y) { $result .= $interval->format("%y years "); }
    if ($interval->m) { $result .= $interval->format("%m months "); }
    if ($interval->d) { $result .= $interval->format("%d days "); }
    if ($interval->h) { $result .= $interval->format("%h hours "); }
    if ($interval->i) { $result .= $interval->format("%i minutes "); }
    if ($interval->s) { $result .= $interval->format("%s seconds "); }

    return $result;
}

$first_date = new DateTime("2012-11-30 17:03:30");
$second_date = new DateTime("2012-12-21 00:00:00");

$difference = $first_date->diff($second_date);

echo format_interval($difference);

7

  • DateTime() is not a function, it’s an object, and it’s there since PHP 5.2. Make sure that your server supports it.

    Jul 22, 2013 at 16:21

  • 2

    @SecondRikudo DateTime::Diff need PHP 5.3.0

    – PhoneixS

    Jul 7, 2014 at 9:21


  • We have a problem, exchange first_date to second_date and we’re getting same result? Why not say 0 days 0 hours 0 minutes 0 seconds or only 0. Example: 2012-11-30 17:03:30 – 2012-12-21 00:00:00 and 2012-12-21 00:00:00 – 2012-11-30 17:03:30 get same result.

    Sep 22, 2017 at 8:49

  • Because diff gives you the difference between the two times. The difference is not 0 regardless of which date comes later.

    Sep 22, 2017 at 16:11

  • 1

    This is a really good answer as it provides a clear function that can be called from anywhere in a codebase without lots of time calcs. Others answers allow you to drop echoed calcs on the fly that address the symptoms rather than solve the problem… The only element I’ve added (and pretty much all other posts don’t cover this) is the pluralisation of $interval elements if more than 1.

    – nickhar

    Jan 3, 2019 at 23:12