Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP wrong DateTime::diff() returns wrong DateInterval

I have an issue with a difference of two Datetime. Here is the command line to display the DateInterval object :

php -r "\$a = new Datetime('first day of 4 months ago midnight'); \$b = new Datetime('first day of 1 month ago midnight'); var_dump(\$a->diff(\$b));"

And here the DateInterval output :

class DateInterval#3 (15) {
  public $y =>      int(0)
  public $m =>      int(3)
  public $d =>      int(3)
  public $h =>      int(0)
  public $i =>      int(0)
  public $s =>      int(0)
  public $weekday =>               int(0)
  public $weekday_behavior =>      int(0)
  public $first_last_day_of =>     int(0)
  public $invert =>                int(0)
  public $days =>                  int(92)
  public $special_type =>               int(0)
  public $special_amount =>             int(0)
  public $have_weekday_relative =>      int(0)
  public $have_special_relative =>      int(0)
}

Edit: The first and second Datetime:

class DateTime#1 (3) {
  public $date =>
  string(19) "2014-03-01 00:00:00"
  public $timezone_type =>
  int(3)
  public $timezone =>
  string(13) "Europe/Zurich"
}

class DateTime#2 (3) {
  public $date =>
  string(19) "2014-06-01 00:00:00"
  public $timezone_type =>
  int(3)
  public $timezone =>
  string(13) "Europe/Zurich"
}

Notice the 3 days! I'm on PHP 5.5.8 but I'm sure that this DateInterval had 0 months a few days ago. The DateInterval output 0 days in PHP 5.4.28 and the 5.5.14. I'm not sure that the PHP version has an effect.

In both cases, the days property is 92.

like image 910
rgazelot Avatar asked Sep 05 '25 16:09

rgazelot


2 Answers

Providing insight into Paul T. Rawkeen's answer, the problem with DateTime::diff is that it first converts the timezone to UTC before computation.

<?php

$zurich = new DateTimeZone('Europe/Zurich');
$utc = new DateTimeZone('UTC');

$a = new DateTime('first day of 4 months ago midnight',$zurich);
$b = new DateTime('first day of 1 month ago midnight',$zurich);

var_dump($a,$b);

$a->setTimezone($utc);
$b->setTimezone($utc);

var_dump($a,$b);

?>

Gives the following:

object(DateTime)[3]
  public 'date' => string '2014-03-01 00:00:00' (length=19)
  public 'timezone_type' => int 3
  public 'timezone' => string 'Europe/Zurich' (length=13)
object(DateTime)[4]
  public 'date' => string '2014-06-01 00:00:00' (length=19)
  public 'timezone_type' => int 3
  public 'timezone' => string 'Europe/Zurich' (length=13)
object(DateTime)[3]
  public 'date' => string '2014-02-28 23:00:00' (length=19)
  public 'timezone_type' => int 3
  public 'timezone' => string 'UTC' (length=3)
object(DateTime)[4]
  public 'date' => string '2014-05-31 22:00:00' (length=19)
  public 'timezone_type' => int 3
  public 'timezone' => string 'UTC' (length=3)

The 3 day discrepancy is now very clear, after the timezone is converted from Europe/Zurich to UTC the dates are now 2014-02-28 23:00:00 and 2014-05-31 22:00:00 for $a and $b respectively.


The solution is to work entirely in UTC and convert before displaying the DateTime:

<?php

$zurich = new DateTimeZone('Europe/Zurich');
$utc = new DateTimeZone('UTC');

$a = new DateTime('first day of 4 months ago midnight',$utc);
$b = new DateTime('first day of 1 month ago midnight',$utc);

var_dump($a,$b);

$a->setTimezone($zurich);
$b->setTimezone($zurich);

var_dump($a,$b);

?>

Notice that all days are now 01, albeit the hours are now a little different (see the note at the end of this answer):

object(DateTime)[3]
  public 'date' => string '2014-03-01 00:00:00' (length=19)
  public 'timezone_type' => int 3
  public 'timezone' => string 'UTC' (length=3)
object(DateTime)[4]
  public 'date' => string '2014-06-01 00:00:00' (length=19)
  public 'timezone_type' => int 3
  public 'timezone' => string 'UTC' (length=3)
object(DateTime)[3]
  public 'date' => string '2014-03-01 01:00:00' (length=19)
  public 'timezone_type' => int 3
  public 'timezone' => string 'Europe/Zurich' (length=13)
object(DateTime)[4]
  public 'date' => string '2014-06-01 02:00:00' (length=19)
  public 'timezone_type' => int 3
  public 'timezone' => string 'Europe/Zurich' (length=13)

To offer a bit of insight into this phenomenon, note the following:

  • February has 28 days (in 2014)
  • May has 31 days
  • DST starts on March 30: +01:00
  • Europe/Zurich is UTC+02:00

When converting from Europe/Zurich to UTC one must consider more than just the year, month and day, but the hours, minutes, and seconds too. If this was any other day than the first of any month, this problem would not occur, however the hours would still be 23:00 and 22:00 (pre the examples above).

like image 121
zamnuts Avatar answered Sep 07 '25 17:09

zamnuts


This thing depends on DateTimeZone you provide.

If you set Europe/Zurich or any EEST time you will get the described result.

If GMT/UTC for e.g., you will get $d = 0.


You can use global time zone definition along your project to avoid such problems (if it suits you)

date_default_timezone_set( "Europe/Zurich" );

or define required time zone for DateTime objects.


UPD: as was mentioned below in comment, by @mudasobwa, this problem is mentioned here about 3 years ago.

like image 31
Paul T. Rawkeen Avatar answered Sep 07 '25 18:09

Paul T. Rawkeen