1

I am implementing a table with a timestamp and a calculated value based on the timestamp (the calculation does not matter for my question) in PHP.

I noticed a very strange behavior in PHP's DateTime object when using the add method at the transistion from summer time to winter time (daylight saving time).

In my example, I am adding 15 minutes to the timestamp and printing it (using local format, unix utc timestamp and timestamp offset in seconds):

<?php

date_default_timezone_set('Europe/Vienna');

$offset = new DateInterval("PT15M");

foreach([new DateTime("2016-03-27 01:00:00"),
         new DateTime("2016-10-30 01:00:00")] as $dt) {
    $lastTs = NULL;

    for($j = 0; $j < 12; $j++) {
        echo $dt->format('d.M.Y H:i:s P (U)');

        if(!is_null($lastTs))
            echo ' (+' . ($dt->format('U') - $lastTs) . ')';

        $lastTs = $dt->format('U');

        echo "\n";

        $dt->add($offset);
    }

    echo "\n";
}

This little script gives me a table like this (notice the huge jump on 30.Oct.2016 02:15):

27.Mar.2016 01:00:00 +01:00 (1459036800)
27.Mar.2016 01:15:00 +01:00 (1459037700) (+900)
27.Mar.2016 01:30:00 +01:00 (1459038600) (+900)
27.Mar.2016 01:45:00 +01:00 (1459039500) (+900)
27.Mar.2016 03:00:00 +02:00 (1459040400) (+900)
27.Mar.2016 03:15:00 +02:00 (1459041300) (+900)
27.Mar.2016 03:30:00 +02:00 (1459042200) (+900)
27.Mar.2016 03:45:00 +02:00 (1459043100) (+900)
27.Mar.2016 04:00:00 +02:00 (1459044000) (+900)
27.Mar.2016 04:15:00 +02:00 (1459044900) (+900)
27.Mar.2016 04:30:00 +02:00 (1459045800) (+900)
27.Mar.2016 04:45:00 +02:00 (1459046700) (+900)

30.Oct.2016 01:00:00 +02:00 (1477782000)
30.Oct.2016 01:15:00 +02:00 (1477782900) (+900)
30.Oct.2016 01:30:00 +02:00 (1477783800) (+900)
30.Oct.2016 01:45:00 +02:00 (1477784700) (+900)
30.Oct.2016 02:00:00 +02:00 (1477785600) (+900)
30.Oct.2016 02:15:00 +01:00 (1477790100) (+4500)
30.Oct.2016 02:30:00 +01:00 (1477791000) (+900)
30.Oct.2016 02:45:00 +01:00 (1477791900) (+900)
30.Oct.2016 03:00:00 +01:00 (1477792800) (+900)
30.Oct.2016 03:15:00 +01:00 (1477793700) (+900)
30.Oct.2016 03:30:00 +01:00 (1477794600) (+900)
30.Oct.2016 03:45:00 +01:00 (1477795500) (+900)

On 27. Mar everything looks correct. But when going back to winter time, there is a huge jump. I dont think this is how DST works.

Instead, I really wanted to see this ouput (edited in notepad):

30.Oct.2016 01:45:00 +02:00 (1477784700) (+900)
30.Oct.2016 02:00:00 +02:00 (1477785600) (+900)
30.Oct.2016 02:15:00 +02:00 (1477786500) (+900)
30.Oct.2016 02:30:00 +02:00 (1477787400) (+900)
30.Oct.2016 02:45:00 +02:00 (1477788300) (+900)
30.Oct.2016 02:00:00 +01:00 (1477789200) (+900)
30.Oct.2016 02:15:00 +01:00 (1477789200) (+900)
30.Oct.2016 02:30:00 +01:00 (1477791000) (+900)
30.Oct.2016 02:45:00 +01:00 (1477791900) (+900)
30.Oct.2016 03:00:00 +01:00 (1477792800) (+900)
30.Oct.2016 03:15:00 +01:00 (1477793700) (+900)

Now 02:00 occurs 2 times but with different offsets (and corret unix timestamps).

What changes to my code are needed to get the correct result like shown above?

feedc0de
  • 3,646
  • 8
  • 30
  • 55

1 Answers1

1

There is no way to achieve it with DateTime object with Europe/Vienna timezone.

PHP does not store timestamps, but local time + timezone Check this answer for details. When you do $dt->format('U') it converts local time to timestamp. 30.Oct.2016 02:15:00 (Europe/Vienna) can be resolved to 2 timestamps: 1477786500 (Sun, 30 Oct 2016 00:15:00 UTC) and 1477790100 (Sun, 30 Oct 2016 01:15:00 UTC). Since the ambiguity, PHP picks the later one, which breaks your calculations.

The workaround is to use UTC timezone for any date-time manipulations, and convert it to local timezone only for output:

$utc = new DateTimeZone('utc');
$viena = new DateTimeZone('Europe/Vienna');

$offset = new DateInterval("PT15M");

foreach([new DateTime("2016-03-26 23:00:00", $utc),
         new DateTime("2016-10-29 23:00:00", $utc)] as $dt) {
    $lastTs = NULL;

    for($j = 0; $j < 12; $j++) {
        $local = clone $dt;
        $local->setTimeZone($viena);
        echo $local->format('d.M.Y H:i:s P'); // <== the only place where you need local timezone
        echo $dt->format(' (U)');

        if(!is_null($lastTs))
            echo ' (+' . ($dt->format('U') - $lastTs) . ')';

        $lastTs = $dt->format('U');

        echo "\n";

        $dt->add($offset);
    }

    echo "\n";
}
Community
  • 1
  • 1
Alex Blex
  • 34,704
  • 7
  • 48
  • 75
  • I just noticed that MySQL does the same using DATETIME's. Should I store all timestamps using UNIX Timestamps as any abstraction like DATETIME simply doesn't calculate correctly? – feedc0de May 03 '17 at 14:11
  • Either, timestamps, or DateTime with UTC timezone does the job. – Alex Blex May 03 '17 at 14:22
  • Do you have any clue why MySQL NOW() does not output UTC by default? Do the mysql-devs want you to store local time into DATETIME's? – feedc0de May 03 '17 at 14:25