1

I'm crunching some numbers from a time recording experiment using PHP. How do I add two DateInterval-objects together?

One of the DateIntervals are found as such (the difference between two DateTime-objects):

$start_stamp = new \DateTime( '2017-09-01T07:10' ); // DateTime-object
$end_stamp   = new \DateTime( '2017-09-01T07:10' ); // DateTime-object
$duration    = $start_stamp->diff( $end_stamp ); // DateInterval-object

The other DateInterval is simply define as such:

$abc = new \DateInterval( 'PT1H' ); // DateInterval with duration of 1 hour

I've read a couple of places, that one should convert my DateIntervals to DateTime-objects and then use the $dateTimeObject->add(...)-function. But that seems silly to me. It is a duration, - so why should I convert it to something that it isn't is, in order to handle it properly.

I wrote below-written function to solve the problem, - but it seems clumsy. Can it be written better - or are there a better way of summing up two DateInterval-objects?

function sumDateIntervals($dateinterval_one, $dateinterval_two)
{
  $d1_years = $dateinterval_one->y;
  $d1_months = $dateinterval_one->m;
  $d1_days = $dateinterval_one->d;
  $d1_hours = $dateinterval_one->h;
  $d1_minutes = $dateinterval_one->i;
  $d1_seconds = $dateinterval_one->s;
  $d1_float = $dateinterval_one->f;

  $d2_years = $dateinterval_two->y;
  $d2_months = $dateinterval_two->m;
  $d2_days = $dateinterval_two->d;
  $d2_hours = $dateinterval_two->h;
  $d2_minutes = $dateinterval_two->i;
  $d2_seconds = $dateinterval_two->s;
  $d2_float = $dateinterval_two->f;

  $return_dateinterval = new DateInterval( 
                     'P' . ( string ) ( $d1_years + $d2_years ) . 'Y' 
                     . ( string ) ( $d1_months + $d2_months ) . 'M' 
                     . ( string ) ( $d1_days + $d2_days ) . 'DT' 
                     . ( string )  ($d1_hours + $d2_hours ) . 'H' 
                     . ( string ) ( $d1_minutes + $d2_minutes ) . 'M' 
                     . ( string ) ($d1_seconds + $d2_seconds ) . 'S' );

  return $return_dateinterval;
}
Zeth
  • 2,273
  • 4
  • 43
  • 91

2 Answers2

2

2021 Update

Almost four years wiser, I think the best way is to let PHP compute the sum and normalize the fields in the process:

function sumDateIntervals(DateInterval $a, DateInterval $b)
{
  $base = new DateTimeImmutable();

  return $base->add($a)->add($b)->diff($base);
}

Check it online.

The originally accepted answer

To me, your solution looks like the best way to implement this functionality.

There is no need to explicitly convert the sums to strings (PHP does it anyway). If you want to make it more readable you can use sprintf() to produce the string and shorter names for the arguments:

function sumDateIntervals(DateInterval $a, DateInterval $b)
{
    return new DateInterval(sprintf('P%dY%dM%dDT%dH%dM%dS',
        $a->y + $b->y,
        $a->m + $b->m,
        $a->d + $b->d,
        $a->h + $b->h,
        $a->i + $b->i,
        $a->s + $b->s
    ));
}
axiac
  • 68,258
  • 9
  • 99
  • 134
  • There is a problem with this as recently as PHP8. There is no overflow built-into this so it will make nonsensical and hard to reason about ISO DateIntervals. Try `var_dump(sumDateIntervals(new DateInterval('P0DT30S'), new DateInterval('P0DT30S')));`, which I would expect to come back with `P0Y0M0DT0H1M0S` instead it comes back with `P0Y0M0DT0H0M60S`, which might be equivalent, but imagine any where time-component S or M are both > 30, H are both > 12. Also think about Days, Months, Years. It's a difficult one to solve well I think. – MrMesees Jun 20 '21 at 04:16
  • It always worked this way: https://3v4l.org/9tCkj#focus=eol – axiac Jul 01 '21 at 09:28
  • It's not if it works, but what burden such DateInterval input strings have on a reviewer, troubleshooting engineer, etc. – MrMesees Jul 01 '21 at 14:19
0

An alternative might be to use the magic __set_state() method to create a new DateInterval, building an array of the property values that are needed from the sums of the equivalent properties in the two existing intervals.

A quick and ugly block of code (untested) to demonstrate this approach:

$properties = ['y' => PHP_INT_MAX,'m' => 12,'d' => 30,'h' => 24,'i' => 60,'s' => 60];


$dti1 = new DateInterval('P1Y7M16DT15H30M15S');
$dti2 = new DateInterval('P2Y8M26DT14H40M55S');

$state = array_fill_keys(array_keys($properties), null);

$carry = 0;
foreach(array_reverse($properties) as $property => $limit) {
    $state[$property] = $dti1->{$property} + $dti2->{$property} + $carry;
    $carry = 0;
    if ($state[$property] > $limit) {
        do {
            $state[$property] -= $limit;
            ++$carry;
        } while ($state[$property] > $limit);
        $state[$property] = $state[$property] % $limit;
    }
}

$newInterval = DateInterval::__set_state($state);

var_dump($newInterval);

It needs a bit of cleaning up to handle the day property, and to allow for the f property that was added in PHP 7.1

Mark Baker
  • 209,507
  • 32
  • 346
  • 385
  • Hmm... This seems quite advanced. There are several things that I don't quite get. What's the advantage of doing it this way over the way that I suggested? – Zeth Oct 16 '17 at 11:05
  • There aren't necessarily advantages, just a different approach.... although the `limit` logic tries to keep individual property values within an appropriate range, so you don't get (for example) `PT47H` but `P1DT23H` – Mark Baker Oct 16 '17 at 11:33