ID: 49081 Updated by: dani...@php.net -Summary: DateTime::diff calculates diffs incorrectly Reported By: nate at frickenate dot com Status: Assigned Bug Type: Date/time related Operating System: * PHP Version: 5.3.0 Assigned To: derick New Comment:
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 Previous Comments: ------------------------------------------------------------------------ [2009-12-16 05:44:52] 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 ------------------------------------------------------------------------ [2009-07-27 22:55:24] 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" } ------------------------------------------------------------------------ -- Edit this bug report at http://bugs.php.net/?id=49081&edit=1