php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #72004 DateTime comparison unexpected behavior
Submitted: 2016-04-11 16:27 UTC Modified: -
Votes:1
Avg. Score:5.0 ± 0.0
Reproduced:1 of 1 (100.0%)
Same Version:1 (100.0%)
Same OS:0 (0.0%)
From: mcloide1977 at gmail dot com Assigned:
Status: Open Package: Date/time related
PHP Version: 5.6.20 OS: Ubuntu 14.04.3 LTS
Private report: No CVE-ID: None
Have you experienced this issue?
Rate the importance of this bug to you:

 [2016-04-11 16:27 UTC] mcloide1977 at gmail dot com
Description:
------------
When comparing 2 DateTime objects using comparison operators such as greater than, or less than, etc, you would expect that if DateTime object 1 has a timestamp that is greater than DateTime object 2 that it would return a truthful statement when being compared. 

While running some unit tests I managed to get some failures on a comparison test that simply did is $now >= $earlier. It, in all possible variations tested, failed where $now was never greater than or equals the $earlier DateTime object. That changed whenever there was a direct timestamp comparison happening or when the getTimeStamp method from the $now DateTime object.

Before submitting this bug I have checked this bug report https://bugs.php.net/bug.php?id=68078  in which I had the hope that fixed the issue. It did not.

The following test script was ran under:

PHP 5.6.20-2+deb.sury.org~trusty+1 (cli)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies
    with Zend OPcache v7.0.6-dev, Copyright (c) 1999-2016, by Zend Technologies

and under

PHP 5.6.19 (cli) (built: Mar 14 2016 11:55:51)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies

With the same end result.

A copy of this test script can be found in: https://gist.github.com/mcloide/eb7db1cc38fe4c6476388902bda724c3

 

Test script:
---------------
<?php
foreach (['EST', 'EDT', 'CST', 'CDT', 'PST', 'PDT'] as $tz) {

echo "---- TZ: $tz ---- \n";

$now = new DateTime('@' . (new DateTime('11:30 AM EST'))->getTimeStamp());
$earlier = new DateTime('7 AM', new DateTimeZone($tz));

echo $now->format('r') . "\n";
echo $earlier->format('r') . "\n";

$mod = $now->setTimeZone(new DateTimeZone($tz));

echo $now->format('r') . "\n";
echo $earlier->format('r') . "\n";

echo (int)($earlier < $now) . "\n";

echo $now->getTimestamp() . "\n";

echo (int)($earlier < $now) . "\n";
echo "------------------\n\n";
}

Expected result:
----------------
When performing this comparison:

echo (int)($earlier < $now) . "\n";

Regardless if getTimeStamp method is called the end result should be true.

---- TZ: CDT ----
Mon, 11 Apr 2016 16:30:00 +0000
Mon, 11 Apr 2016 07:00:00 -0600
Mon, 11 Apr 2016 10:30:00 -0600
Mon, 11 Apr 2016 07:00:00 -0600
1
1460392200
1
------------------

Actual result:
--------------
---- TZ: CDT ----
Mon, 11 Apr 2016 16:30:00 +0000
Mon, 11 Apr 2016 07:00:00 -0600
Mon, 11 Apr 2016 10:30:00 -0600
Mon, 11 Apr 2016 07:00:00 -0600
0
1460392200
1
------------------

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2016-04-11 17:28 UTC] cmshelto at gmail dot com
Interestingly enough, only happens with the shorter timezone names (PDT, PST) and not the longer ones (America/Los_Angeles):

<?php

$now = new \DateTime ('@1460394772');
$now -> setTimezone (new \DateTimeZone ('PDT'));

printf ("(PDT) now->format('U'): %s\n", $now -> format ('U'));
$now -> getTimestamp();
printf ("(PDT) now->format('U'): %s\n", $now -> format ('U'));

echo "\n";

$now = new \DateTime ('@1460394772');
$now -> setTimezone (new \DateTimeZone ('America/Los_Angeles'));

printf ("(America/Los_Angeles) now->format('U'): %s\n", $now -> format ('U'));
$now -> getTimestamp();
printf ("(America/Los_Angeles) now->format('U'): %s\n", $now -> format ('U'));



Expected
--------------
(PDT) now->format('U'): 1460394772
(PDT) now->format('U'): 1460394772

(America/Los_Angeles) now->format('U'): 1460394772
(America/Los_Angeles) now->format('U'): 1460394772


Actual
-------------
(PDT) now->format('U'): 1460365972
(PDT) now->format('U'): 1460394772

(America/Los_Angeles) now->format('U'): 1460394772
(America/Los_Angeles) now->format('U'): 1460394772




I am not actually sure which timestamp I would expect to see (1460365972 or 1460394772) - but I am 100% sure I would not expect to get a different value back after calling getTimestamp()
 [2016-04-11 17:30 UTC] crussell52 at gmail dot com
It is worth noting, the example illustrates that the comparison works as expected *after* calling `$now->getTimeStamp()`.

From the source, it looks likes the compare function conditionally invokes `timelib_update_ts()`.  By comparison, `date_timestamp_get` unconditionally invokes `timelib_update_ts()`.

This suggests that the value of `o2->time->sse_uptodate` is incorrect in the described scenario. 

Reference: 

https://github.com/php/php-src/blob/PHP-5.6.20/ext/date/php_date.c#L2192
static int date_object_compare_date(zval *d1, zval *d2 TSRMLS_DC)
{
	...
	if (!o1->time->sse_uptodate) {
		timelib_update_ts(o1->time, o1->time->tz_info);
	}
	if (!o2->time->sse_uptodate) {
		timelib_update_ts(o2->time, o2->time->tz_info);
	}
        ....
}

https://github.com/php/php-src/blob/PHP-5.6.20/ext/date/php_date.c#L3645
PHP_FUNCTION(date_timestamp_get)
{
	...
	timelib_update_ts(dateobj->time, NULL);
        ...
}
 [2016-04-11 19:19 UTC] crussell52 at gmail dot com
Expanding on the point made by cmshelto at gmail dot com...

The same exact (incorrect) behavior occurs when using offsets for the timezone:

Test script:
------------
<?php

$now = new \DateTime ('@1460394772');
$now -> setTimezone (new \DateTimeZone ('-0700'));

printf ("(PDT) now->format('U'): %s\n", $now -> format ('U'));
$now -> getTimestamp();
printf ("(PDT) now->format('U'): %s\n", $now -> format ('U'));

echo "\n";

$now = new \DateTime ('@1460394772');
$now -> setTimezone (new \DateTimeZone ('America/Los_Angeles'));

printf ("(America/Los_Angeles) now->format('U'): %s\n", $now -> format ('U'));
$now -> getTimestamp();
printf ("(America/Los_Angeles) now->format('U'): %s\n", $now -> format ('U'));



Expected
--------------
(PDT) now->format('U'): 1460394772
(PDT) now->format('U'): 1460394772

(America/Los_Angeles) now->format('U'): 1460394772
(America/Los_Angeles) now->format('U'): 1460394772


Actual
-------------
(PDT) now->format('U'): 1460365972
(PDT) now->format('U'): 1460394772

(America/Los_Angeles) now->format('U'): 1460394772
(America/Los_Angeles) now->format('U'): 1460394772


The two incorrect cases have a common code path in `timelib_unixtime2local()`.  In the case of `America/Los_Angeles`, there is special logic to reset `tm->sse` to counter the fact that an earlier call to `timelib_unixtime2gmt()` modifies it.  The other two cases (TZ-as-abbreviation, TZ-as-offset) do not do this.

So far, this is the only place that I have been able to find which:

1) Manipulates `time->sse`
2) Follows the same code path for abbreviation and offset timezones
3) Follows a different path for timezone ids (e.g. `America/Los_Angeles`)
 [2016-04-11 19:26 UTC] crussell52 at gmail dot com
Code reference for my earlier comment about suspect code path:

https://github.com/php/php-src/blob/master/ext/date/lib/unixtime2tm.c#L179
void timelib_unixtime2local(timelib_time *tm, timelib_sll ts)
{
	...
	switch (tm->zone_type) {
		case TIMELIB_ZONETYPE_ABBR:
		case TIMELIB_ZONETYPE_OFFSET: {
			int z = tm->z;
			signed int dst = tm->dst;

			timelib_unixtime2gmt(tm, ts - (tm->z * 60) + (tm->dst * 3600));

			tm->z = z;
			tm->dst = dst;
			break;
		}

		case TIMELIB_ZONETYPE_ID:
			gmt_offset = timelib_get_time_zone_info(ts, tz);
			timelib_unixtime2gmt(tm, ts + gmt_offset->offset);

			/* we need to reset the sse here as unixtime2gmt modifies it */
			tm->sse = ts;
			tm->dst = gmt_offset->is_dst;
			tm->z = gmt_offset->offset;
			tm->tz_info = tz;

			timelib_time_tz_abbr_update(tm, gmt_offset->abbr);
			timelib_time_offset_dtor(gmt_offset);
			break;

		default:
			tm->is_localtime = 0;
			tm->have_zone = 0;
			return;
	}
        ...
}
 
PHP Copyright © 2001-2020 The PHP Group
All rights reserved.
Last updated: Wed Oct 28 03:01:23 2020 UTC