Search code examples
phpdatetimedatediff

Calculate Absolute Differences between Dates in PHP


I'm trying to create a function that I can use to find the difference between 2 DateTimes, but I want to be able to return the absolute value of each one.

Say the difference is 15 minutes 20 seconds, I want to be able to output 15 minutes, 20 seconds, or 920 seconds. And this will also expand into days, months, years. So 2 days 12 minutes could be 2 days, 48 hours, 2892 minutes, or 173520 seconds.

I originally started with my own code but wanted to account for proper differences when bringing days, months, and years into account, so I changed to using DateDiff which I've now found doesn't work how I want.

This is my code so far

define( 'MINUTE_IN_SECONDS', 60 );
define( 'HOUR_IN_SECONDS',   60 * MINUTE_IN_SECONDS );
define( 'DAY_IN_SECONDS',    24 * HOUR_IN_SECONDS   );
define( 'WEEK_IN_SECONDS',    7 * DAY_IN_SECONDS    );
define( 'MONTH_IN_SECONDS',  30 * DAY_IN_SECONDS    );
define( 'YEAR_IN_SECONDS',  365 * DAY_IN_SECONDS    );

/**
 * Output a length between times in friendly format
 *
 * @param string $date1 Date/Time from in Y-m-d H:i:s format.
 * @param string $date2 Date/Time to in Y-m-d H:i:s format.
 * @param string $length Length of output. Set to "l" for detailed down to the second. Defaults to short if nothing entered.
 * @param string $format Format to return the output in. Based on {@link https://www.php.net/manual/en/dateinterval.format.php PHP Date Interval Format}.
 * If no format is set, the function will try assume the best output
 *
 * @return string Will return the requested output.
*/
function friendlyDtmDiff($date1, $date2, $length = '', $format = '') {

  // Create DateTime for diff()
  $dt1 = new \DateTime($date1);
  $dt2 = new \DateTime($date2);

  // Check if $dt2 ($date2) is before $dt1 ($date1) and
  // swap the values if they are, also outputing a negative sign
  if($dt2 < $dt1) {
    $dtHolder = $dt1->format('Y-m-d H:i:s');
    $dt1 = new \DateTime($dt2->format('Y-m-d H:i:s'));
    $dt2 = new \DateTime($dtHolder);
    $r = '-';
  } else {
    $r = '';
  }

  // Set the interval
  $interval = $dt1->diff($dt2);

  // Assume best output options
  if(empty($format) || $format == '') {
    // Difference in seconds
    $diffSecs = $dt2->getTimestamp() - $dt1->getTimestamp();
    if($diffSecs > YEAR_IN_SECONDS) { // Assume Years
      $format = 'y';
    } else if($diffSecs > MONTH_IN_SECONDS) { // Assume Months
      $format = 'm';
    } else if($diffSecs > DAY_IN_SECONDS) { // Assume Days
      $format = 'd';
    } else if($diffSecs > HOUR_IN_SECONDS) { // Assume Hours
      $format = 'h';
    } else if($diffSecs > MINUTE_IN_SECONDS) { // Assume Minutes
      $format = 'i';
    } else {// Assume seconds
      $format = 's';
    }
  }

  switch ($format) {
    // Return seconds
    case 's':
      return $interval->format('%s seconds');
      break;

    // Return seconds padded
    case 'S':
      return $interval->format('%S seconds');
      break;

    // Return minutes
    case 'i':
      return ($length == 'l') ? $r . $interval->format('%i minutes and %s seconds') : $r . $interval->format('%i minutes') ;
      break;

    // Return minutes padded
    case 'I':
      return ($length == 'l') ? $r . $interval->format('%I minutes and %S seconds') : $r . $interval->format('%I minutes') ;
      break;

    // Return hours
    case 'h':
      return ($length == 'l') ? $r . $interval->format('%h hours, %i minutes, and %s seconds') : $r . $interval->format('%h hours') ;
      break;

    // Return hours padded
    case 'H':
      return ($length == 'l') ? $r . $interval->format('%H hours, %I minutes, and %S seconds') : $r . $interval->format('%H hours') ;
      break;

    // Return days
    case 'a':
    case 'd':
      return ($length == 'l') ? $r . $interval->format('%d days, %h hours, %i minutes, and %s seconds') : $r . $interval->format('%d days') ;
      break;

    // Return days padded
    case 'D':
      return ($length == 'l') ? $r . $interval->format('%D days, %H hours, %I minutes, and %S seconds') : $r . $interval->format('%D days') ;
      break;

    // Return months
    case 'm':
      return ($length == 'l') ? $r . $interval->format('%m months, %d days, %h hours, %i minutes, and %s seconds') : $r . $interval->format('%m months') ;
      break;

    // Return months padded
    case 'M':
      return ($length == 'l') ? $r . $interval->format('%M months, %D days, %H hours, %I minutes, and %S seconds') : $r . $interval->format('%M months') ;
      break;

    default:
      return 'not available';
      break;
  }
}

This is an example of code

$dtmNow = new \DateTime();
$authExpireDtm = new \DateTime();
$authExpireDtm->modify('+15 minutes');

echo friendlyDtmDiff($dtmNow->format('Y-m-d H:i:s'), $authExpireDtm->format('Y-m-d H:i:s'), 'l', 's');

Which I want to output 900 seconds, but it outputs 0 seconds and this is the object that the $interval has

DateInterval Object ( [y] => 0 [m] => 0 [d] => 0 [h] => 0 [i] => 15 [s] => 0 [f] => 0 [weekday] => 0 [weekday_behavior] => 0 [first_last_day_of] => 0 [invert] => 0 [days] => 0 [special_type] => 0 [special_amount] => 0 [have_weekday_relative] => 0 [have_special_relative] => 0 ) 

Which as you can see has nothing (0) under the s item, so of course my function is returning 0 seconds. I'm wanting to know how to get it to return the 900 seconds. I know it's easy enough when comparing time (hours, minutes seconds), but I want to get it accurate when converting days, months, years.

So if I put in 2020-02-28 00:00:00 and 2020-03-01 00:00:00 it will return 172800 seconds or 2880 minutes, but if I put in 2019-02-28 00:00:00 and 2019-03-01 00:00:00 it will return 86400 seconds or 1440 minutes

Is anyone able to point me in the right direction to achieve the result I'm after


Solution

  • Here's a function that does 99% of what you want (it doesn't implement rounding when length != 'l', and it also doesn't remove the s from e.g. 1 years). 0 values are not output, although you could add a parameter to control that.

    function friendlyDtmDiff($date1, $date2, $length = '', $format = '') {
        // Create DateTime for diff()
        $dt1 = new \DateTime($date1);
        $dt2 = new \DateTime($date2);
    
        // Create intervals
        if ($dt1 < $dt2) {
            $sign = '';
            $interval = $dt1->diff($dt2);
        }
        else {
            $sign = '-';
            $interval = $dt2->diff($dt1);
        }
        // Output format (minimum 2 digits for upper case formats)
        $of = ($format < 'a') ? '%02d' : '%d';
    
        // generate output using an array of terms to be imploded
        $output = array();
        // create time components
        switch ($format) {
            case 'Y':
            case 'y':
                $years = $interval->y;
                if ($years) $output[] = sprintf("$of years", $years);
                if ($length != 'l') break;
                $interval->y = 0;
            case 'M':
            case 'm':
                $months = $interval->y * 12 + $interval->m;
                if ($months) $output[] = sprintf("$of months", $months);
                if ($length != 'l') break;
                $interval->m = $interval->y = 0;
            case 'D':
            case 'd':
                $days = ($interval->y * 12 + $interval->m) * 30 + $interval->d;
                if ($days) $output[] = sprintf("$of days", $days);
                if ($length != 'l') break;
                $interval->d = $interval->m = $interval->y = 0;
            case 'H':
            case 'h':
                $hours = (($interval->y * 12 + $interval->m) * 30 + $interval->d) * 24 + $interval->h;
                if ($hours) $output[] = sprintf("$of hours", $hours);
                if ($length != 'l') break;
                $interval->h = $interval->d = $interval->m = $interval->y = 0;
            case 'I':
            case 'i':
                $minutes = ((($interval->y * 12 + $interval->m) * 30 + $interval->d) * 24 + $interval->h) * 60 + $interval->i;
                if ($minutes) $output[] = sprintf("$of minutes", $minutes);
                if ($length != 'l') break;
                $interval->i = $interval->h = $interval->d = $interval->m = $interval->y = 0;
            case 'S':
            case 's':
                $seconds = (((($interval->y * 12 + $interval->m) * 30 + $interval->d) * 24 + $interval->h) * 60 + $interval->i) * 60 + $interval->s;
                if ($seconds) $output[] = sprintf("$of seconds", $seconds);
                break;
            default:
                return 'Invalid format';
                break;
        }
        // put the output string together
        $last = array_pop($output);
        return $sign . (count($output) ? implode(', ', $output) . ' and ' : '') . $last;
    }
    

    Example usage:

    echo friendlyDtmDiff('2020-02-28 00:00:00', '2020-03-01 12:00:56', '', 'h') . PHP_EOL;
    echo friendlyDtmDiff('2020-02-28 00:00:00', '2020-03-01 12:00:56', 'l', 'h') . PHP_EOL;
    echo friendlyDtmDiff('2020-02-28 00:00:00', '2020-03-01 12:08:56', 'l', 'h') . PHP_EOL;
    echo friendlyDtmDiff('2018-12-28 00:00:00', '2020-04-11 04:08:56', 'l', 'y') . PHP_EOL;
    echo friendlyDtmDiff('2018-12-28 00:00:00', '2020-04-11 04:08:56', 'l', 'm') . PHP_EOL;
    echo friendlyDtmDiff('2018-12-28 00:00:00', '2020-04-11 04:08:56', 'l', 'd') . PHP_EOL;
    echo friendlyDtmDiff('2018-12-28 00:00:00', '2020-04-11 04:08:56', 'l', 'h') . PHP_EOL;
    echo friendlyDtmDiff('2018-12-28 00:00:00', '2020-04-11 04:08:56', 'l', 'i') . PHP_EOL;
    echo friendlyDtmDiff('2018-12-28 00:00:00', '2020-04-11 04:08:56', 'l', 's') . PHP_EOL;
    

    Output:

    60 hours
    60 hours and 56 seconds
    60 hours, 8 minutes and 56 seconds
    1 years, 3 months, 14 days, 4 hours, 8 minutes and 56 seconds
    15 months, 14 days, 4 hours, 8 minutes and 56 seconds
    464 days, 4 hours, 8 minutes and 56 seconds
    11140 hours, 8 minutes and 56 seconds
    668408 minutes and 56 seconds
    40104536 seconds
    

    Demo on 3v4l.org