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: 2021-04-07 16:19 UTC
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: cmb (profile)
Status: Not a bug Package: Date/time related
PHP Version: 5.6.20 OS: Ubuntu 14.04.3 LTS
Private report: No CVE-ID: None
Welcome back! If you're the original bug submitter, here's where you can edit the bug or add additional notes.
If you forgot your password, you can retrieve your password here.
Password:
Status:
Package:
Bug Type:
Summary:
From: mcloide1977 at gmail dot com
New email:
PHP Version: OS:

 

 [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

Pull Requests

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;
	}
        ...
}
 [2021-04-07 16:19 UTC] cmb@php.net
-Status: Open +Status: Not a bug -Assigned To: +Assigned To: cmb
 [2021-04-07 16:19 UTC] cmb@php.net
From the documentation[1]:

| Please do not use any of the timezones listed here (besides
| UTC), they only exist for backward compatible reasons, and may
| expose erroneous behavior.

So this is not a bug.

[1] <https://www.php.net/manual/en/timezones.others.php>
 
PHP Copyright © 2001-2025 The PHP Group
All rights reserved.
Last updated: Wed Feb 05 20:01:30 2025 UTC