PHP DateTime difference – it’s a trap!

This is a story of a “WTF is going on” when you don’t pay attention and assume you know what’s going on behind the scenes.

What

A schedule. Simple as that. Start date – which is 2016-12-15. And from that date 23 days of events filled in calendar.

Now I want to change this start date to 2017-03-01 and update dates in events. Because I use DateTime object the idea is very simple. I need to compute the difference between the old and new start date, and add that difference to other dates to get, obviously, new dates. Notice words “difference” and “add”. Luckily there is a ->diff() and ->add() functions. Yay!

So the loop will look like this:

$schedule = get_schedule();
$diff = $schedule[0]->getDate()->diff($newStartDate); // get difference
foreach($schedule as $s){
   $s->setDate($s->getDate()->add($diff)); //add difference to create new dates
}

Cool. Simple.

Not…

What just happened?!

I assume I want a difference and then want to add it. And because function names are, well, exactly what I want, I don’t overthink it. It was working great until it didn’t. Until it was 2017 and we made a jump from December to March with a February in the middle. You might think, naughty 28 days February.

After making this change we lost 3 days of events. Instead of 23 days of events there were 20. When debugging all dates, loop and stuff, it turns out that 6 different days after ->add() became 3 new days. To be exact:

  • 29-12-2016 and 01.01.2017 – became 15.01.2017
  • 30-12-2016 and 02.01.2017 – became 16.01.2017
  • 31-12-2016 and 03.01.2017 – became 17.01.2017

Wait, what? Just to be clear, the first dates (29,30,31 Dec) were correct. Since 01.01.2017 everything went bazinga.

WTF trap

So what’s the problem? I assumed that making a simple diff and then add everything would work very well. And because php works a lot on guesses – you know, like he knows when you want “2” to be a string and when an integer and stuff – and there was no time and blah blah, I didn’t expected to be a trap. But there was.

Lets take a closer look at returning object from ->diff() function:

DateInterval Object
(
    [y] => 0
    [m] => 2
    [d] => 14
    [h] => 0
    [i] => 0
    [s] => 0
    [weekday] => 0
    [weekday_behavior] => 0
    [first_last_day_of] => 0
    [invert] => 0
    [days] => 76
    [special_type] => 0
    [special_amount] => 0
    [have_weekday_relative] => 0
    [have_special_relative] => 0
)

Nothing fancy here.

Ok, so now the ->add()ing part.

//We have this date object $date1
DateTime Object
(
    [date] => 2016-12-29 00:00:00.000000
    [timezone_type] => 3
    [timezone] => US/Pacific
)

//And we have this date object $date2
DateTime Object
(
    [date] => 2017-01-01 00:00:00.000000
    [timezone_type] => 3
    [timezone] => US/Pacific
)


//Let's do the $date1->add($diff); and see what we have here
DateTime Object
(
    [date] => 2017-03-15 00:00:00.000000
    [timezone_type] => 3
    [timezone] => US/Pacific
)

//And the $date2->add($diff); and what's here
DateTime Object
(
    [date] => 2017-03-15 00:00:00.000000
    [timezone_type] => 3
    [timezone] => US/Pacific
)

Two different dates, adding same difference, and yet exact same result. Again. WTF?

->add() function

Ok ok, but there is a February in the middle. And this is always a tricky month. Not only because sometimes it has 29 days, but because it has less than 30 days. ;o) But that’s not the point.

I assumed that after computing a difference it will add that difference. With days or something. But the ->add() function doesn’t work like that. It adds, but not days.

DateTime::adddate_addAdds an amount of days, months, years, hours, minutes and seconds to a DateTime object

So instead of adding 76 days, it added 2 months and 14 days. So:

29.12.2016 + 2 months = 29.02.2017. We know February has only 28 days this years, so php is trying to be clever and adds this one day. So we have instead 01.03.2017. Plus 14 days = 15.03.2017.

01.01.2017 + 2 months = 01.03.2017. See where it’s going? Plus 14 days = 15.03.2017.

Two different dates, same difference, same result. After that, not anymore that WTF. I mean it kinda is, but when you know what EXACTLY this ->add() function is adding, it’s more like, fine, you win php, but you are still odd.

Solution

Explicitly say you want days and use ->modify() function. This is what I came up under a pressure of – fix this shit asap. Works so far:

$date1->modify('+ '.$diff->days.' days');

When you say, dear php modify this date and please add 76 days, works as expected.

29.01.2016 becomes – 15.03.2017 and 01.01.2017 becomes – 18.03.2017, and everyone is happy.

Except client who lost 3 days in database. But LONG LIVE BACKUPS! So everything is fine.

On a side note

To make the solution full, we need to actually consider that the difference could be 1 day, and then it should say day instead of days. Also, that we can change dates to the older date than currently set, so there could also be a need for “-” instead of “+”.  So the actual solution to this when there is a bit of a time to focus is:

$changeThatMuch = ($diff->invert==1 ? '- ' : '+ ') . $diff->days . ' day' . ($diff->days>1 ? 's' : '');
$d->modify($changeThatMuch);

You can see some debugging here.

0 0 vote
Article Rating