Relative Date Strings in PHP

Manipulating dates and times in any language is a difficult prospect. The problem is that while there are absolutes in time (seconds, months, weeks), there are also plenty of ambiguous or varying values (‘month’ for example). In this post, I will discuss the use of ‘relative’ date strings in PHP, and how they can cause some problems for you.

First off, let’s just look at some example code. Code to make you sigh heavily.

<?php 
$date   = "July 31, 2013"; 
$date_1 = strtotime($date);
$date_2 = strtotime('+1 month', $date_1);
$date_3 = strtotime('-1 month', $date_1);

echo date("F j, Y", $date_1) . '<br />';
echo date("F j, Y", $date_2) . '<br />'; 
echo date("F j, Y", $date_3);

Run this code in your favourite webserver of choice (so long as it runs PHP) and you will get the following output:

July 31, 2013
August 31, 2013
July 1, 2013

Let’s look at what we got back. PHP’s strtotime() function did what we expected with $date_1. It converted it to an epoch, and then the date() function correctly rendered it back to July 31, 2013. Then, we added one month to it, again using strtotime(). And it did exactly what we expected, we got August 31, 2013. But we also subtracted a month from it. And now we get July 1, 2013. WAT?! Now, you perceptive ones out there are going to say, oh hey, that makes perfect sense. It’s just normalizing a ‘month’ to 30 days, right?

**Wrong. **

Change the date in $date to January 29, 2013, and re-run your script.

Yeah, I know.

And you can’t just arbitrarily decide that you’re always going to tell strtotime() to use ‘+30 days’ or ‘-30 days’, because you may be on January 29, and want the end of next month (February 28th or 29th, depending on year), and you may end up with March 1st. Not what you intended.

BUT WHY?

So why is this happening? Is it further proof that PHP is the Nickelback of programming languages? No, it’s actually part of the GNU standard. An excellent article (actually, whole book) has already been written on this by Derick Rethans, wherein he explains that this is due to PHP taking something like July 31, 2013 and going one month back, making it June 31, 2013, which is an invalid date and automatically falling back to July 1, 2013 which is the equivalent timestamp.

Think about it in terms of epochs. If you were able to calculate an epoch for June 31, 2013 00:00:00 it would be the same epoch as July 1, 2013 00:00:00, so PHP logically gives you the ‘right’ value when we convert it using the date() function.

There is further documentation for this in the GNU tar manual, which explains it at a lower level and reveals that this is how the date binary on Linux/Unix systems works. So, before you go hating on PHP, realize that PHP is doing what the underlying architecture is telling it to do.

BUT HOW DO I FIX IT?

As documented in the above article from Mr. Rethans, if you are on PHP 5.3 or later, you have the luxury of using the modify() function of the DateTime() construct and expanding your relative string to be ‘last day of last month’ or ‘first day of next month’ and PHP will do exactly what you want it to do.

Or you can normalize further by calculating your month, picking something like the 15th (which every month has) and going one month back from there. Then, there is the ‘t’ parameter to the PHP date() function, which will give you the number of days in that calendar month, for that calendar year. This is particularly useful, because if you can calculate what the previous month was, you can then use date(‘t’, $timestamp) to get the last day of the month. You have to be careful and accommodate for the December-January crossover, of course. For example:

<?php 

$date = "March 29, 2013";
$date_1 = strtotime($date);

$month = date('m', $date_1);
$year = date('Y', $date_1);

if ($month === 1) {
    $month = 12;
    $year--;
} else {
    $month--;
}

$lastMonth = strtotime("$month/15/$year");
$lastDay = date('t', $lastMonth);
$timestamp = strtotime("$month/$lastDay/$year");
echo date("F j, Y", $date_1) . '<br />';
echo date("F j, Y", $timestamp);

This code will behave as intended, regardless of which date you put in. Is it more work to get what you want? Of course. When are you going to use this kind of calculation? Any trending that is month-by-month, where you need to be able to say that you are measuring from the first second of a month to the last second of the month. That will change depending on which month you are calculating.

Calculating dates and times is a common ‘gotcha’ that developers face every single day. And while it’s easy for some to hate on PHP, in this particular case, PHP behaves the same as SQLite, or your GNU tools. Leave the hate for Nickelback.

Published 12 Jul 2013

Writing better code by building better JavaScript
Don Burks on Twitter