'Normalize DateInterval in PHP

The situation/issue

I have an issue with DateInterval in PHP.

Given the following code

$interval = new DateInterval("PT6000S");
echo $interval->h; // 0
echo $interval->i; // 0
echo $interval->s; // 6000
echo $interval->format("%h:%i"); // 0:0

I want this to represent 1 hour and 40 minutes, not 6000 seconds.

The question

Is there a built-in way to normalize the DateInterval? A specific way of writing the duration string? Or is this something that must be done normally?

I can modify the way it's created and formated if anyone has a "work-around" to suggest.

I have done my research, but strangely enough I have not found anything helpful with this issue.



Solution 1:[1]

The DateInterval class is a bit annoying like that. Strictly it's doing the right thing, since your interval spec string specified zero minutes and hours, but it's definitely not helpful that it doesn't roll over excess seconds into minutes and hours.

One option I've used before is to add the interval to a new DateTime object initialised to the Unix epoch, then format the resulting time instead. This would mean using the standard date format strings (so h rather than %h, etc)

echo (new DateTime('@0'))->add(new DateInterval("PT6000S"))->format('h:i');
// 01:40

See https://3v4l.org/OX6JF

Solution 2:[2]

The solution

Like every time I ask for help, I cannot stop trying and I generally find the answer right after. No exception this time.

I've found a user contributed note in the DateInterval documentation.

You have to create two DateTime (they can be any date and time), add your interval to the second and substract the first from the second. This will populate the DateInterval in a normalized way.

Here is the code the user wrote, wrapped in a handy-dandy function:

public function createDateInterval(string $duration): DateInterval
{
    $d1 = new DateTime();
    $d2 = new DateTime();
    $d2->add(new DateInterval($duration));
    return $d2->diff($d1);
}

(Note that this function can throw an exception if your $duration string is not correctly formatted.)

Solution 3:[3]

This solution uses DateTime with a fixed time. Why ?

DateTime also supports microseconds. There is therefore a certain probability that 2 calls to new Date() will not return the same time. Then we can get wrong results.

//360000 Sec = 100 hours = 4 Days + 4 hours
$di = new DateInterval("PT360000S");

$diNormalize = date_create('@0')->diff(date_create('@0')->add($di));

var_export($diNormalize);
/*
DateInterval::__set_state(array(
   'y' => 0,
   'm' => 0,
   'd' => 4,
   'h' => 4,
   'i' => 0,
   's' => 0,
   'f' => 0.0,
   'weekday' => 0,
   'weekday_behavior' => 0,
   'first_last_day_of' => 0,
   'invert' => 0,
   'days' => 4,
   'special_type' => 0,
   'special_amount' => 0,
   'have_weekday_relative' => 0,
   'have_special_relative' => 0,
)) 
*/

echo $diNormalize->format('%a days %h hours %i minutes');
//4 days 4 hours 0 minutes 

DateInterval has a special format method for the output. This should be used here and no detour via date, gmdate or DateTime should be taken.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 iainn
Solution 2 Noah Boegli
Solution 3 jspit