Tutorials

Working with dates in PHP

Views: 98982
Rating: 4/5
Votes: 4

Preface

There are many topics on the forums [/forums] that go again, topics many people often are having trouble with. One of these problem areas are how to handle dates, convert them to a different format, timezone issues, etc. This tutorial will attempt to address many of the commonest problems related to date and time issues.

Storing date and time in a database

Before covering how you handle dates in PHP, I want to talk a little about how you should store dates in a database. Specifically I'm going to talk about MySQL because that is what I've got the most experience with. The other DBMS have also got facilities for working with date and time information, so you can check out the manual for these.

Many people "invent" their own way of storing dates in the database, and this inevitably gives them some trouble later on. They may for instance have a VARCHAR field type and store dates in a DD/MM/YYYY format. This is the wrong way to do it.

There are a number of different ways you can store dates properly in a database. One way is to have an integer field and store a Unix timestamp. I don't like this approach. It makes you unable to use MySQL's date and time functions, and there are the potential issues with the Y2K38 problem [http://en.wikipedia.org/wiki/Y2K38].

MySQL has a datatype called TIMESTAMP. This displays the date and time in an ISO 8601 [http://en.wikipedia.org/wiki/ISO_8601] format, i.e. from the most significant part (year) to the least significant part (second). An example could be 2009-08-18 9:17:21. The extra formatting isn't actually necessary when typing it in, so 2009081891721 would be the same thing.

There is also a datatype called DATETIME. First looking at it, it seems identical to the TIMESTAMP, but that is not the case. First of all, TIMESTAMP still have issues with the Y2K38 thing, so it can only store dates that can be represented with a 32-bit unsigned integer starting from the Unix epoch (1970-01-01 00:00:00). This gives an upper limit 2038-01-19 03:14:07. DATETIME does not have this problem; it can store dates from 1000-01-01 to 9999-12-31. Finally, there are some datatypes called DATE and TIME. These are for if you just need either the date or time part only.

My recommendation is to use either DATETIME, DATE or TIME depending on your needs. Actually, there is a YEAR type as well. Using MySQL's built-in datatypes for date and time storage allows you to use its date and time functions [http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html].

I'll not be using databases for the remainder of this tutorial. The examples will assume you already have some string containing some date.

Timezones

This is a very frequent problem. When using the date() function in PHP it is always using the server time for generating date and time stamps. Unless you're in a different timezone than your server, this is not a problem. I have got a VPS (Virtual Private Server) in England, but I live in Denmark. That is a time difference of one hour. How do I get PHP to display the right time for me?

Because I am the server admin, I can just change the server's clock (and that is actually what I've done). Most people cannot just do that, so we'll have to work out some alternative solutions to this problem.

If you've got access to php.ini (or can change its directives using an Apache .htaccess file) you can change the date.timezone [http://php.net/manual/en/datetime.configuration.php#ini.date.timezone] directive (actually, this needs to be set to something if you don't want errors from PHP). Otherwise you can use the date_default_timezone_set() [http://php.net/date_default_timezone_set] function. The manual has a list of valid timezones [http://php.net/manual/en/timezones.php] you can use for these two things. Because I am in Denmark, I would choose Europe/Copenhagen.

Let's try it out:

echo date('H:i:s');

My computer's local time is 09:34:08, so that is also the output. Now we'll try to change the timezone:

date_default_timezone_set('America/New_York');
echo date('H:i:s');

And we'll get 03:34:08. So it seems to be working (how fortunate!). This is a very good way of doing it.

What if we need multiple dates in different timezones, but on the same page? Well, that is quite simple. Consider the following example:

date_default_timezone_set('Europe/Copenhagen');
echo 'The time in Copenhagen is: ' . date('H:i:s') . PHP_EOL;

date_default_timezone_set('America/New_York');
echo 'The time in New York is: ' . date('H:i:s') . PHP_EOL;

date_default_timezone_set('Europe/Moscow');
echo 'The time in Moscow is: ' . date('H:i:s') . PHP_EOL;

The output of that would currently be:

The time in Copenhagen is: 09:45:56
The time in New York is: 03:45:56
The time in Moscow is: 11:45:56

What if you have users from multiple timezones? Still simple! Ask them what their timezone is and set it accordingly. The function timezone_identifiers_list() [http://php.net/timezone_identifiers_list] gives you an array of all the timezone identifiers, so you can use that to generate a drop down list they can choose from. You can also try out geolocating, but that is beyond the scope of this tutorial.

Finally, PHP has a function called gmdate() [http://php.net/gmdate], which always formats the date in GMT. If you know the offset in seconds you can do like:

gmdate('H:i:s', time() + $offset);

, so you might do

gmdate('H:i:s', time() + 3600);

for an offset of one hour. However, this requires you to take DST [http://en.wikipedia.org/wiki/DST] into consideration. Not all countries observe DST, and some do it differently than others. My recommendation would be to use PHP's built-in support for timezones.

Converting from one format to another

"I have got a date in DD/MM/YYYY but I need it in MM-DD-YYYY. Help!"

How do we help this person? Well, let's analyze the problem a bit. Each date consists of three components: the day, the month and the year. So, essentially what we need to do is reorder it and use hyphens instead of slashes. There are a few ways we can accomplish this. Let's first try splitting it up. We've got a function called explode() [http://php.net/explode] for that.

$oldDate = '18/08/2009'; // DD/MM/YYYY

$parts = explode('/', $oldDate);

/**
 * Now we have:
 *   $parts[0]: the day
 *   $parts[1]: the month
 *   $parts[2]: the year
 * We could also have done:
 *   list($day, $month, $year) = explode('/', $oldDate);
 */

$newDate = "{$parts[1]}-{$parts[0]}-{$parts[2]}"; // MM-DD-YYYY

echo $newDate; // 08-18-2009

This seems to work pretty nice. We can also use something called regular expressions. This will essentially be a one-liner:

$oldDate = '18/08/2009';
$newDate = preg_replace('#^(\d{2})/(\d{2})/(\d{4})$#', '$2-$1-$3', $oldDate);

I am not going to explain regular expressions in this tutorial. You may check out this regex tutorial [http://www.phpfreaks.com/tutorial/regular-expressions-part1---basic-syntax] for that.

Any other ways? Yes, there is! As it usually happens to be with programming, there are many ways to do just one thing. Assuming we have already split up our date in three variables $day, $month and $year we can use a function called mktime() [http://php.net/mktime] for generating a Unix timestamp and then use date() to format it in this way:

$timestamp = mktime(0, 0, 0, $month, $day, $year);
echo date('m-d-Y', $timestamp);

Yet another way is using strtotime() [http://php.net/strtotime] to convert a formatted string into a Unix timestamp. EXCEPT, strtotime() does not recognize DD/MM/YYYY as a valid date format, so it returns false. There are also problems with ambiguity. Consider the string 07-08-2009. Is that the 7th of August, or is it the 8th of July? It will be the former.

Date validation

If you take a date as input (for instance a birthday), it would be quite useful knowing if it's actually a valid date. There are two conditions that must be met before a date can be considered valid:

  1. It must conform to a particular format (we might only want YYYY-MM-DD for instance).
  2. The date must actually exist (2009-02-29 doesn't exist, but 2008-02-29 does).

The first condition is rather simple, and we can do that using regular expressions. The second one is a bit more involved. We will have to take the length of a month into consideration. Maybe it's a leap year, maybe it's not? Fortunately, we needn't worry about that; PHP has a function called checkdate() [http://php.net/checkdate] that, uhm... checks a date. So, regex and checkdate() is what we need. A little example:

<?php
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['date'])) {
	if (preg_match('#^(\d{4})-(\d{2})-(\d{2})$#', $_POST['date'], $matches) && checkdate($matches[2], $matches[3], $matches[1])) {
		echo '<p>The date is <strong>valid</strong>!</p>';
	}
	else {
		echo '<p>The date is <strong>invalid</strong>!</p>';
	}
	
	echo '<hr>';
}
?>

<form action="<?php echo $_SERVER['PHP_SELF'] ?>" method="post">
	<label for="date">Enter an ISO 8601 date:</label>
	<input type="text" name="date" id="date"<?php if (!empty($_POST['date'])): ?> value="<?php echo htmlentities($_POST['date']) ?>"<?php endif ?>>
	<button type="submit">Check validity</button>
</form>

This example should be fairly straightforward. First we check the format of the string using preg_match() where we also extract the month, day and year in backreferences. preg_match() returns true if the string matches the pattern. Because logical and is left to right associative, the preg_match() is evaluated first, and the checkdate() is only evaluated if the first operand is true (if the left operand was false, the AND expression couldn't possibly return true, so evaluating the right operand would be redundant, hence the reason why it isn't).

If you have made separate fields for day, month and year it's even simpler and we can skip checking the format.

You might setup additional conditions that must be met before you consider a date valid. You might ask for a date interval meaning that the first must be earlier than the last. You might only want dates in the past or in the future, or they might have to be within a particular preset interval. The easiest way of doing this would be to convert the dates to Unix timestamps. From hereon it's rudimentary integer comparison. I'll leave that as an exercise to you. A safer bet might (recall Y2K38) be to use DateTime::diff(). For that, see this comment in the manual [http://php.net/manual/en/datetime.diff.php#92692].

Conclusion and further reading

As you have hopefully seen. Dates aren't really that difficult to work with. PHP's built-in functions makes it very easy to validate and format all kinds of dates. It really does all the heavy lifting for us. Therefore, it shouldn't come as a surprise that the recommended further reading for this topic is the date reference section in the manual [http://www.php.net/manual/en/book.datetime.php]. PHP also has native support for other calendar types [http://www.php.net/manual/en/book.calendar.php] such as the Jewish calendar. I even think someone wrote a book solely about PHP and dates. You can go Google it (or Bing it).