php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #74885 Wrong behaviour when comparing dates with microseconds
Submitted: 2017-07-09 14:30 UTC Modified: 2017-08-17 09:05 UTC
Votes:1
Avg. Score:5.0 ± 0.0
Reproduced:1 of 1 (100.0%)
Same Version:1 (100.0%)
Same OS:1 (100.0%)
From: marcospassos dot com at gmail dot com Assigned: derick (profile)
Status: Closed Package: Date/time related
PHP Version: 7.1.7 OS:
Private report: No CVE-ID: None
 [2017-07-09 14:30 UTC] marcospassos dot com at gmail dot com
Description:
------------
Modifying the microsecond part of a DateTime leads to wrong behavior in comparison operators.

Test script:
---------------
$expected = new DateTimeImmutable('2015-08-31 01:02:04.456790');
$date = (new DateTimeImmutable('2015-08-31 01:02:03.456789'))->modify('+1000001 microseconds');

// Expected false, but it's actually true
assertFalse($expected > $date || $expected < $date);

Expected result:
----------------
True

Actual result:
--------------
False

Patches

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2017-07-09 14:57 UTC] requinix@php.net
-Assigned To: +Assigned To: derick
 [2017-07-09 14:57 UTC] requinix@php.net
Surely assertTrue($expected == $date) is simpler?

Anyway, it's a floating-point issue where 0.456789 + .000001 != 0.456790
@derick can say whether this is a bug to fix or not.

As a workaround you can compare YmdHisu strings, which will have the 'u' part rounded to 6 places. https://3v4l.org/YlCDm
 [2017-07-09 15:01 UTC] derick@php.net
I'm pretty sure that this is due to floating point rounding, but perhaps there is a way to "fix" this (although I doubt it).
 [2017-07-09 15:04 UTC] nikic@php.net
@derick: Why are the microseconds being stored as a floating point fraction? Can we store them as integer microseconds instead? I remember seeing some other bug reports also relating to float precision issues with the microseconds.
 [2017-07-09 15:14 UTC] marcospassos dot com at gmail dot com
@requinix yes, sure. However, this is so because it's how PHPUnit handles DateTime comparison, and how I came up with this issue.

A better workaround for those facing the same issue:

// 1000000 microseconds (1 second):
// https://bugs.php.net/bug.php?id=74885
// @todo Remove it when this bug get fixed
$microseconds = $this->microseconds % 1000000;
$seconds = intval($this->microseconds / 1000000);

return $date->modify(
    sprintf('%d seconds %d microseconds', $seconds, $microseconds)
);
 [2017-07-09 15:30 UTC] requinix@php.net
That may work for your example but it does not work in the general case.
https://3v4l.org/c54Dt

The problem is not in how the DateTime is modified but in how it is compared. Specifically, in how the microseconds values are compared. As things stand, if you require microsecond-level precision then you must use your own custom comparison strategy.
 [2017-07-09 15:49 UTC] marcospassos dot com at gmail dot com
I can't understand why it's internally stored as float, neither why DateInverval exposes the `f` property as float, once microseconds cannot be expressed as a fraction in PHP.
 [2017-07-09 16:00 UTC] requinix@php.net
Because most of the code supporting DateTime is actually a library.
https://github.com/derickr/timelib

If you missed it, @nikic had just asked @derick about why microseconds are floating-point, so let's step back and let them work.
 [2017-07-10 09:56 UTC] derick@php.net
@nikic - Why? That's a good question that I don't have an answer to. The problem with changing it now is that it is an API break in the library :-/
 [2017-08-17 09:05 UTC] derick@php.net
-Status: Assigned +Status: Closed
 [2017-08-17 09:05 UTC] derick@php.net
The fix for this bug has been committed.

Snapshots of the sources are packaged every three hours; this change
will be in the next snapshot. You can grab the snapshot at
http://snaps.php.net/.

 For Windows:

http://windows.php.net/snapshots/
 
Thank you for the report, and for helping us make PHP better.

This is fixed for PHP 7.2(beta3):

derick@singlemalt:~ $ php -v
PHP 7.2.0beta3 (cli) (built: Aug 17 2017 09:57:54) ( NTS DEBUG )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.2.0-dev, Copyright (c) 1998-2017 Zend Technologies

[PHP: 7.2.0-beta3] derick@singlemalt:~ $ cat /tmp/74885.phpt 
<?php
$expected = new DateTimeImmutable('2015-08-31 01:02:04.456790');
$date = (new DateTimeImmutable('2015-08-31 01:02:03.456789'))->modify('+1000001 microseconds');

// Expected false, but it's actually true
var_dump($expected > $date || $expected < $date);
?>

[PHP: 7.2.0-beta3] derick@singlemalt:~ $ php /tmp/74885.phpt
bool(false)


As I said before, it breaks API/ABI for existing PHP versions, so it won't be fixed there.
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Thu Dec 05 07:01:30 2024 UTC