QDateTime timezone issue

Login or register to post comments
15 replies [Last post]
Offline
Joined: 12/02/2011

The problem originates from memcached usage in my application. I'm using a datagrid to show times of events in my application. These times are a QDateTime objects with no data part, just time.

After adding a caching support I've noticed that all times became one hour less. for example, if an event happened in 11:04:24, It would be shown as if it was happened in 10:04:24.
Turning the caching support off, solves the problem. I've developed this problem deeper and found that caching is not broken, I can reproduce the same QDateTime behavior with serialize/unserialize functions that are used internally by the caching subsystem.

I've ended up with a sample.php (see an attach). just replace the default one in you qcubed folder, run it and press the button. You'll see something like this:

s1: QDateTime Object ( [blnDateNull:protected] => 1 [blnTimeNull:protected] => [strSerializedData:protected] => [date] => 2012-02-03 11:05:24 [timezone_type] => 3 [timezone] => Asia/Novosibirsk )

strTime1=11:05:24

restored after cache: QDateTime Object ( [blnDateNull:protected] => 1 [blnTimeNull:protected] => [strSerializedData:protected] => 2012-02-03T11:05:24+0700 [date] => 2012-02-03 11:05:24 [timezone_type] => 1 [timezone] => +07:00 )

strTime2=10:05:24

I'm using strftime to achive a customized date and time presentation:
$strTime2 = strftime("%H:%M:%S", $dttEvt->Timestamp);

The problem with one-hour time shift does not appear if a standard qcubed qFormat is used:

<?php
$strTime2
= $dtt->qFormat('hhhh:mm:ss');
?>

but in a QQuery.class.php I'm using strftime to format date and time in different ways that can not be achived with qFormat:

<?php
public function GetDataGridHtml() {
// ...
if($this->strType == QDatabaseFieldType::Time)
    return
sprintf('(%s) ? strftime("%%H:%%M:%%S", %s->Timestamp) : null', $strToReturn, $strToReturn);
if(
$this->strType == QDatabaseFieldType::Date)
    return
sprintf('(%s) ? strftime("%%a %%d.%%m.%%G", %s->Timestamp) : null', $strToReturn, $strToReturn);
if(
$this->strType == QDatabaseFieldType::DateTime)
    return
sprintf('(%s) ? strftime("%%a %%d.%%m.%%G %%H:%%M:%%S", %s->Timestamp) : null', $strToReturn, $strToReturn);
// ...
}
?>

From this I conclude that QDateTime object can not be serialized/deserialized correctly (the behaviour of strftime("format", $dtt->Timestamp) is incorrect for unserialized $dtt object). Can someone elaborate on this problem, please?

I can workaround this bug in my code. The simple trick is:

<?php
$dttRestored
= unserialize($strSerializedTime);
$dtt = QDateTime::Now();
$dtt->Hour = $dttRestored->Hour;
$dtt->Minutes = $dttRestored->Minutes;
$dtt->Seconds = $dttRestored->Seconds;
// $dtt can be used to print out the correct time
?>

but this bug is related to caching support in qcubed in general, and all caching users can suffer from it.

Also note the cases in my version of GetDataGridHtml function - the difference from the standard qcubed version is that I distinguish not only QDatabaseFieldType::Time type, but QDatabaseFieldType::Date and QDatabaseFieldType::DateTime also, and format it with custom formatting. The feature request for the next release comes from here: provide a way to specify custom localized format to be used in this function without modifying the QQuery.class.php file directly. But the bug with strftime("format", $dtt->Timestamp) for deserialized $dtt should be fixed first, to make it possible to use strftime here.

thank you for your attention

Offline
Joined: 12/02/2011

the problem with timezone can be solved in this way:
includes/qcubed/_core/framework/QDateTime.class.php

<?php
       
public function __wakeup() {
           
parent::__construct($this->strSerializedData);
            if (
$this->blnDateNull) {
               
parent::setTimezone(new DateTimeZone(date_default_timezone_get()));
            }
        }
?>

svn diff includes/qcubed/_core/framework/QDateTime.class.php

Index: includes/qcubed/_core/framework/QDateTime.class.php
===================================================================
--- includes/qcubed/_core/framework/QDateTime.class.php (revision 2780)
+++ includes/qcubed/_core/framework/QDateTime.class.php (working copy)
@@ -193,6 +193,9 @@
}
public function __wakeup() {
parent::__construct($this->strSerializedData);
+ if ($this->blnDateNull) {
+ parent::setTimezone(new DateTimeZone(date_default_timezone_get()));
+ }
}

/**

Don't know how correct it is, but it works for me.

vakopian's picture
Offline
Joined: 04/08/2008

olegabr,

I don't think there is a bug here. When you do
strftime("format", $dtt->Timestamp)
you are only using the timestamp information from the DateTime object, which knows nothing about the time zone.
Why not use $dtt->format() (which will use DateTime::format())?

From what I see in the code, the current serializtion of QDateTime correctly handles the timezone information since it's using the ISO8601 format to serialize.
With your change you are resetting it back to the default time zone, which may not be what the original object had.

Offline
Joined: 12/02/2011

<?php
date_default_timezone_set
('Asia/Novosibirsk');

$dttNow1 = new QDateTime('11:05:24');
$tm1 = $dttNow1->Timestamp;

$dttNow2 = new QDateTime('11:05:24');
$strSerialized = serialize($dttNow2);

$dser = unserialize($strSerialized);
$tm2 = $dser->Timestamp;

$diff = $tm1 - $tm2;
?>

this code returns diff=3600

ISO8601 is cool, but why I can not expect the same Timestamp value from the original and serialized/deserialized QDateTime object?

Offline
Joined: 12/02/2011

Why not use $dtt->format() (which will use DateTime::format())?

good point, I'll try it. but I have not only to format date but also use it in calculations, compare with current date-time, for example, and actually my code brokes because of comparisons of Timesatamps shifter by one hour. qcubed own datetime comparison function use timestamp internally, so they are broken also. it means that I can not compare non-serialized (QDateTime::Now()) and serialized times now. if it is not a bug, what it is?

vakopian's picture
Offline
Joined: 04/08/2008

olegabr,
You're right there is a bug, but the bug is not in serialization. It's in the QDateTime constructor. Here is an easy demonstration:

<?php
    $strTime
= '2005-05-05T11:05:24+0700';
    print
$strTime."\n";
   
// show that DateTime handles everything correctly
   
$y = new Datetime($strTime);
    print
$y->format(DateTime::ISO8601)." - DateTime\n";
   
// and QDateTime fails miserably
   
$x = new QDatetime($strTime);
    print
$x->format(DateTime::ISO8601)." - QDateTime\n";
?>

This produces
2005-05-05T11:05:24+0700
2005-05-05T11:05:24+0700 - DateTime
2005-05-04T21:05:24-0700 - QDateTime

I already expressed my opinion about the QDateTime constructor (in one word: I think it's crap).

So I just created a ticket http://trac.qcu.be/projects/qcubed/ticket/775, but I don't have the fix yet.

vakopian's picture
Offline
Joined: 04/08/2008

olegabr,

I attached a patch for this issue on the ticket. Let us know if it works for you.

Thanks.

Offline
Joined: 12/02/2011

vakopian,

your patch makes my sample.php work, but the original problem still remains: for fields with type time without timezone in postgresql database I have a one hour shift to the past. For example, it shows 14:00:00 if I've generated the event in 15:00:00. Problem dissapears if caching is turned off.

vakopian's picture
Offline
Joined: 04/08/2008

olegabr,

I'm not sure I understand the problem. Your test case in your comment #3 works fine with the patch. Do you have another simple test case that still fails?

Offline
Joined: 12/02/2011

<?php
$dttNow1
= new QDateTime('11:05:24');
echo
' tm1f=' . strftime("%H:%M:%S", $dttNow1->Timestamp);
echo
' tm1q=' . $dttNow1->qFormat('hhhh:mm:ss');
?>

for me it prints:

tm1f=10:05:24
tm1q=11:05:24

your patch fixes my first test case because it was just a differense between timestamps of the original and deserialized dates. with your patch timestamps are equal, but incorrect in the first place.

It can be fixed by using getTimestamp instead of Timestamp property, like this:

<?php
$dttNow1
= new QDateTime('11:05:24');
echo
' tm1f=' . strftime("%H:%M:%S", $dttNow1->getTimestamp());
echo
' tm1q=' . $dttNow1->qFormat('hhhh:mm:ss');
?>

for me it prints:

tm1f=11:05:24
tm1q=11:05:24

I believe it is because qcubed Timestamp property returns not real timestamp as functions like strftime are expect. So, solution for my problem is to use strftime with getTimestamp, or use QDateTime::format, because it already uses correct timestamp value directly.

The general issue with QDateTime::Timestamp property still remains, I don't know how it should be resolved, but my opinion is that in the current state it is not correct, it's name doesn't reflects it's nature.

vakopian's picture
Offline
Joined: 04/08/2008

olegabr,

As I tried to explain in my first comment in this thread, that's not a bug.
In PHP DateTime object, the timestamp does not contain the timezone information.
That's why are seeing your requests.
Here is an example to demonstrate (note: this is plain DateTime, not QDateTime):

<?php
$dt
= new DateTime('2005-05-05 05:05');
print
$dt->format(DateTime::ISO8601)."\n";
print
$dt->getTimestamp()."\n";

$dt->setTimezone(new DateTimeZone('Asia/Novosibirsk'));
print
$dt->format(DateTime::ISO8601)."\n";
print
$dt->getTimestamp()."\n";
?>

As you can see setting the timezone changes how the datetime is formatted, but keeps the same timestamp.
So taking the timestamp and using it with strftime() will not show the same thing as with format() because timestamp is not aware of the timezone. timezone is kept separately inside the datetime object. You could also see this by simply doing print_r($dt) in the example above before and after setting the timezone.

Offline
Joined: 12/02/2011

vakopian,

yes, you are correct in your points. timezone info is not included in php timestamp. it is by design. it is important to understand this design goal. IMHO it is to allow applications to not worry about timezone at all.

For example, code like this

<?php
$dt
= new DateTime('05:05:05');
print_r($dt);
echo
"<br>";
echo
$dt->getTimestamp();
?>

gives me
DateTime Object ( [date] => 2012-02-13 05:05:05 [timezone_type] => 3 [timezone] => Asia/Novosibirsk )
1329084305

Note there that the date part is filled for me with the now date and the timezone part is filled with my default system timezone. it means that I can write the whole program without any line of timezone management, it is done automagically for me by the php.

php date-time functions like strftime expect timezone-agnostic timestamp AND use the global system timezone to reconstruct the whole picture they need for their job. And again, I can program without managing any timezone stuff.

There are three important date-time parts: the date part, the time part and the timezone part. When I create time-only datetime object I (and php) assume that it is in the current system timezone and date, so, I can obtain a correct timestamp for it as a number of seconds from the 1970-01-01 00:00:00 to the current date and the time I've supplied for DateTime constructor. Then, when I pass this timestamp to strftime, I (and php) assume that the timestamp is in UNIX epoch and timezone is the global system one. And everything works as expected, strftime prints correct values.

BUT! if I use QDateTime::Timestamp, things are different. The code now is:

<?php
require "qcubed.inc.php";
$dt = new QDateTime('05:05:05');
print_r($dt);
echo
"<br>";
echo
$dt->Timestamp;
?>

and it gives me
QDateTime Object ( [blnDateNull:protected] => 1 [blnTimeNull:protected] => [strSerializedData:protected] => [date] => 2012-02-13 05:05:05 [timezone_type] => 3 [timezone] => Asia/Novosibirsk )
946681505

Note the Timestamp value. It is not a UNIX epoch time anymore, it is a number of seconds from the 2000-01-01 00:00:00 to the current day begining, plus the number of seconds from the begining of the current day to the time I've supplied for the QDateTime constructor.

Now, if I pass this qcubed "timestamp" to the strftime function, this function's contract would be broken. Not in timezone part, but in the timestamp part - it is not a valid UNIX epoch timestamp.

This is a problem I'm talking about when I say that QDateTime::Timestamp property is broken. but I don't know the design goals of QDateTime author, may be this QDateTime::Timestamp behavior is by design and by intention? If so, what that intention is? Is it so important that worth to broke the default php timezone-agnostic DateTime paradigm?

--------------------------------------------------

I have more issues to QDateTime object that are closely related to the described one. For example, how would you compare a time obtained from the database as a time without date, with the current time, using QDateTime API?

My solution makes me crying, but I failed to find better one:

<?php
require "qcubed.inc.php";
$dttZero = new QDateTime; // the zero object represents 2000-01-01 00:00:00
$dt = new QDateTime('05:05:05'); // the time-only object from database
echo $dt->Timestamp;
echo
"<br>";
$spanDt = $dt->Difference($dttZero); // number of seconds from the begining of the day to the time specified in $dt object

$dttNow = QDateTime::Now(true); // date and time now object
echo $dttNow->Timestamp;
echo
"<br>";
$
$dttNowDate = QDateTime::Now(false); // date-only now object
$spanNow = $dttNow->Difference($dttNowDate); // number of seconds from the begining of the day to the current moment

if ($spanNow->Seconds > $spanDt->Seconds) {
       
// do something useful
       
echo "now is over 05:05:05";
} else {
        echo
"now is below 05:05:05";
}
?>

It prints

946681505
1329072891
now is below 05:05:05

that is why I can not use QDateTime::LessThen-like functions, they use Timestamp property internally, so they can not compare two QDateTime objects as in the example above. To conclude: my second point is that QDateTime::Timestamp property is broken for date-less QDateTime objects - it handles them differently from the full QDateTime objects with date part set. It violates the default php behavior for such a date-less objects - it does not interprets them as having a current date in a date part.

vakopian's picture
Offline
Joined: 04/08/2008

olegabr,

I'm not sure if this is properly documented, but "time-only" QDateTime objects have "2000-01-01" as the day part. This is different from the standard PHP DateTime objects, which have the current day as the day part for "time-only" objects.

I actually think QDateTime approach is somewhat better.

However, I think you are right that something is still fishy about QDateTime. When constructing a "time-only" object, it should set the date as 2000-01-01 right in the constructor. But it does not do that currently. And that's what you see with print_r() after the constructor.
In fact, it does set the date to '2000-01-01' the moment you call any getter (almost any method call first goes through ReinforceNullProperties(), which "fixes" the date).

Note the Timestamp value. It is not a UNIX epoch time anymore, it is a number of seconds from the 2000-01-01 00:00:00 to the current day begining, plus the number of seconds from the begining of the current day to the time I've supplied for the QDateTime constructor.

No, it is a UNIX epoch timestamp, except at that point the date inside the object is still not "fixed".
So, for example if you call print_r() after you called "echo $dt->Timestamp;", the result would be consistent.

I think using ReinforceNullProperties() on getters is wrong: I don't think getters and "read-only" methods (such as format()) should mutate the object. ReinforceNullProperties() should only be called mutating methods (e.g. on setters) and in the constructor. So I think I will make a patch to fix this.

I still would like to have your use case so it can be tested after I fix this problem.

vakopian's picture
Offline
Joined: 04/08/2008

Here is the ticket with the patch: http://trac.qcu.be/projects/qcubed/ticket/779
Hopefully it will fix your problem too.

vakopian's picture
Offline
Joined: 04/08/2008

olegabr, did you get a chance to try this new patch? Thanks

Offline
Joined: 12/02/2011

vakopian,
last time I've failed to reproduce the original problem, so I've postponed this task now. I'm going to return to it later, sorry for inconvenience.