php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #49081 [PATCH] DateTime::diff() mistake if start in January and interval > 28 days
Submitted: 2009-07-27 22:55 UTC Modified: 2010-05-04 17:12 UTC
Votes:7
Avg. Score:4.9 ± 0.3
Reproduced:6 of 6 (100.0%)
Same Version:1 (16.7%)
Same OS:1 (16.7%)
From: nate at frickenate dot com Assigned: derick (profile)
Status: Closed Package: Date/time related
PHP Version: 5.3.0 OS: *
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: nate at frickenate dot com
New email:
PHP Version: OS:

 

 [2009-07-27 22:55 UTC] nate at frickenate dot com
Description:
------------
DateTime::diff calculates diffs incorrectly.

As an example, the diff of 2009-01-01 and 2009-03-31 *should be* "2 months, 30 days". 2009-01-01 + 2 months = 2009-03-01, + 30 days = 2009-03-31. Taking 2009-01-01 and using DateTime::add() to add 'P2M30D' does indeed result in 2009-03-31. This is correct.

However, running the diff() of 2009-01-01 and 2009-03-31 returns "3 months, 2 days". add()ing 2009-01-01 + 'P3M2D' returns 2009-04-03 instead of 2009-03-31 (as it should, since the diff that told us to add 3M2D was incorrect).

Reproduce code:
---------------
<?

$jan_1 = new DateTime('2009-01-01');
$mar_31 = new DateTime('2009-03-31');

var_dump(date_diff($jan_1, $mar_31));
var_dump($jan_1->add(new DateInterval('P2M30D'))); // correct period
var_dump($jan_1->add(new DateInterval('P3M2D')));  // incorrect period

// END EXAMPLE CODE - following is just extra fluff


// here's the replacement function I am currently
// using to calculate the correct diff until the
// built-in method is patched and functional
function date_diff2 ($t1, $t2) {
    if (! preg_match('/^\d+\z/', $t1) && ($t1 = strtotime($t1)) === false)
        return false;

    if (! preg_match('/^\d+\z/', $t2) && ($t2 = strtotime($t2)) === false)
        return false;

    if ($t1 > $t2)
        list($t1, $t2) = array($t2, $t1);

    $diffs = array(
        'years' => 0, 'months' => 0, 'days' => 0,
        'hours' => 0, 'minutes' => 0, 'seconds' => 0,
    );

    foreach (array_keys($diffs) as $interval) {
        while ($t2 >= ($t3 = strtotime("+1 ${interval}", $t1))) {
            $t1 = $t3;
            ++$diffs[$interval];
        }
    }

    return $diffs;
}


?>

Expected result:
----------------
object(DateInterval)#3 (8) {
  ["y"]=>
  int(0)
  ["m"]=>
  int(2)
  ["d"]=>
  int(30)
  ["h"]=>
  int(0)
  ["i"]=>
  int(0)
  ["s"]=>
  int(0)
  ["invert"]=>
  int(0)
  ["days"]=>
  int(89)
}
object(DateTime)#1 (3) {
  ["date"]=>
  string(19) "2009-03-31 00:00:00"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(16) "America/Montreal"
}
object(DateTime)#1 (3) {
  ["date"]=>
  string(19) "2009-07-03 00:00:00"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(16) "America/Montreal"
}


Actual result:
--------------
object(DateInterval)#3 (8) {
  ["y"]=>
  int(0)
  ["m"]=>
  int(3)
  ["d"]=>
  int(2)
  ["h"]=>
  int(0)
  ["i"]=>
  int(0)
  ["s"]=>
  int(0)
  ["invert"]=>
  int(0)
  ["days"]=>
  int(89)
}
object(DateTime)#1 (3) {
  ["date"]=>
  string(19) "2009-03-31 00:00:00"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(16) "America/Montreal"
}
object(DateTime)#1 (3) {
  ["date"]=>
  string(19) "2009-07-03 00:00:00"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(16) "America/Montreal"
}


Patches

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2009-12-16 05:44 UTC] peter dot schleif at gmx dot de
More simple code to reproduce error:

<?php
   date_default_timezone_set('Europe/Berlin');
   $d1 = new DateTime('2010-01-01 06:00:00');
   $d2 = new DateTime('2010-01-31 10:00:00');
   $d  = $d1->diff($d2);
   print_r($d);
?>

Expected:
---------
   [m] => 0
   [d] => 30

Actual:
-------
   [m] => 1
   [d] => 2
 [2010-01-05 22:11 UTC] danielc@php.net
This bug continues to exist in 5.3.2RC2.

DateTime::diff() / date_diff() chokes on dates starting in January if the interval is greater than 28 days (during non-leap years).

It is happening because of a bug in do_range_limit_days_relative() in ext/date/lib/tm2unixtime.c.  At the end of the function if *d > days_next_month, the code subtracts the days and adds a month.  So when the starting month is January, the days_next_month is 28 or 29, mistakenly triggering the month/day swapping behavior.

Patch:
http://www.analysisandsolutions.com/php/bug49081.diff

Test:
http://www.analysisandsolutions.com/php/bug49081.phpt
 [2010-01-06 01:04 UTC] danielc@php.net
A better test file:
http://www.analysisandsolutions.com/php/bug49081v2.phpt

The scenarios it covers are more relevant and thorough, plus it improves the context, making it easier see what the test is doing.
 [2010-01-06 16:19 UTC] danielc@php.net
Hmm... Isn't the whole days_next_month functionality in do_range_limit_days_relative() unnecessary?
 [2010-01-23 00:10 UTC] danielc@php.net
Here is an updated patch against PHP_5_3.  It removes all of the "next_month" code in 

do_range_limit_days_relative().
http://www.analysisandsolutions.com/php/bug49081v2.53.diff

As with the earlier patch, PHP built fine with it and all of the ext/date tests that passed under 

the unpached PHP_5_3 build continued to pass under the patched version.

Grepping the entire PHP_5_3 code base indicates the only place do_range_limit_days_relative() gets 

called is in timelib_do_rel_normalize(), and the only place that gets called is timelib_diff().

It is unclear what purpose the "next_month" functionality served.  Perhaps it was there in case the 

"base_y" and "base_m" parameters were later than the "y"/"m"/"d" parameters?  But that doesn't apply 

becausetimelib_diff() always passes the earlier date to timelib_do_rel_normalize() as "one" and on to 

do_range_limit_days_relative() as "base_*".
 [2010-05-04 17:11 UTC] derick@php.net
Automatic comment from SVN on behalf of derick
Revision: http://svn.php.net/viewvc/?view=revision&amp;revision=298973
Log: - Fixed bug #49081 (DateTime::diff() mistake if start in January and interval &gt;
  28 days). (Derick)
 [2010-05-04 17:12 UTC] derick@php.net
-Status: Assigned +Status: Closed
 [2010-05-04 17:12 UTC] derick@php.net
This bug has been fixed in SVN.

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/.
 
Thank you for the report, and for helping us make PHP better.


 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Sat Nov 23 10:01:28 2024 UTC