Why A - B + B != A with PHP DateTime and DateInterval?

148 Views Asked by At

Can someone explain to me why adding and subtracting the same DateInterval from DateTime objects results in different dates? Look at the hours: it goes from 20:00 to 19:00 when I subtract the interval, but when I add the interval it's still 19:00.

$date = new DateTime("2015-04-21 20:00", new DateTimeZone('Europe/Berlin'));

$days = 28;
$minutes = $days * 24 * 60;

$interval = new DateInterval("PT{$minutes}M");

var_dump($date);
$date->sub($interval);
var_dump($date);
$date->add($interval);
var_dump($date);

Results in:

object(DateTime)#1 (3) {
  ["date"]=>
  string(26) "2015-04-21 20:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(13) "Europe/Berlin"
}
object(DateTime)#1 (3) {
  ["date"]=>
  string(26) "2015-03-24 19:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(13) "Europe/Berlin"
}
object(DateTime)#1 (3) {
  ["date"]=>
  string(26) "2015-04-21 19:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(13) "Europe/Berlin"
}

It looks to me that $date->sub($interval); changes the hour from 20:00 to 19:00 because of Daylight Saving Time difference (it works as expected with other dates, i.e. 2015-05-21 20:00), but $date->add($interval); doesn't apply DTS. Could it be a bug?

3

There are 3 best solutions below

2
On

It is indeed because of the Daylight saving time. For the given time if you change your Time Zone to 'America/Los_Angeles' for example it would be fine because in LA the time changed in the beginning of March. You can also test with the same time zone putting your date at the start of November then you will see that 28 days before you will get 21:00.

Check here for the supported time zones.

0
On

Using $interval->invert seems to solve the problem:

$date = new DateTime("2015-04-21 20:00", new DateTimeZone('Europe/Berlin'));

$days = 28;
$minutes = $days * 24 * 60;

$interval = new DateInterval("PT{$minutes}M");
$interval->invert = 1;

var_dump($date);
$date->add($interval);
var_dump($date);
$date->sub($interval);
var_dump($date);

Which results in:

object(DateTime)#1 (3) {
  ["date"]=>
  string(26) "2015-04-21 20:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(13) "Europe/Berlin"
}
object(DateTime)#1 (3) {
  ["date"]=>
  string(26) "2015-03-24 19:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(13) "Europe/Berlin"
}
object(DateTime)#1 (3) {
  ["date"]=>
  string(26) "2015-04-21 20:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(13) "Europe/Berlin"
}
0
On

I recently haved a similary problem with this piece of code :

private function convertSecondsToInterval($value)
{
    $dt1 = new \DateTime();
    $dt2 = clone $dt1;
    $dt2->add(new \DateInterval('PT'. $value . 'S'));
    return $dt2->diff($dt1);
}

During night from 2018-03-24 to 2018-03-25 (timezone change from CET to CEST in France), I noticed a strange behavior : the DateInterval result was 1 hour less than expected. This is because by default, the DateTime object is on Europe/Paris timezone. So, for example when adding 7 hours to 2018-03-24 20:00:00 I raise the timezone change (+01:00 to +02:00), resulting in a sum with 1 hour less than expected.

The implemented solution is to initialize the DateTime object in with UTC timezone to skip daylight change problems.

private function convertSecondsToInterval($value)
{
    $dt1 = new \DateTime('now', new \DateTimeZone('UTC'));
    $dt2 = clone $dt1;
    $dt2->add(new \DateInterval('PT'. $value . 'S'));

    return $dt2->diff($dt1);
}