Handling time zones is one of those things that developers would rather not think about; the entire system is built around edge cases, making things more difficult than they need to be. Here’s a quick tutorial on how to manage users’ time zones quickly and easily by making PHP do the heavy lifting.
User Experience
Managing time zones is actually more of a social problem than a technical one, so I’m going to address this issue first. The biggest hurdle in making your PHP application handle time zones correctly is actually asking the users for their time zone. This is primarily because varied adherence to daylight savings time turns 40 simple UTC offsets into more than 400 time zones.
The simple solution is to present the user with every active time zone, including “America/Toronto”, “America/New_York”, “EST”, “GMT -5″, and a slew of other seemingly equivalent time zones for EST, but this just defers the hard work onto the user. Many applications take the opposite extreme and present just one EST option that internally maps to “America/New_York”. This fails to handle regional DST differences properly, though, which leaves a lot to be desired.
For example, Arizona is in MST, but that they don’t observe DST at all. So for several months of the year, customers from Arizona who diligently selected their time zone are seeing the wrong time, since internally “MST” probably actually means “America/Denver”.
Selecting Time Zones
An all-encompassing solution is to create two drop-downs, one with an overall region or the current time, and then a second dynamically-loaded list of specific time zones to provide intra-regional accuracy. I wrote a quick demo of this to show how this would work.
One alternate approach is to make a best guess of who your users are and provide them with a fair balance between accuracy and user experience. This list would be a good start, with just 90 options sorted by the over-arching UTC offset
Smart Solutions
Providing a map for the user to select their location, attempting to obtain the computer’s local time from Javascript, or basing the time zone or time zone options on the IP’s country and latitude/longitude are all viable heuristics to finding out your users’ time zone, but all of these have various pitfalls (most commonly someone not where they normally are). Before attempting to implement a smart, but not smart enough, time zone detection system, it’s worth thinking about how badly time zones affect your users. Providing a map to select a time zone may make sense for a calendar app, where times are a critical component, but may be overkill in other applications.
Ultimately, how you ask your users for their time zone is important and many fail websites to provide adequate options. DST means that time zones are not strictly an offset from UTC; they maintain behavioral differences as well.
Timestamps and UTC
Now that we’ve addressed the social implications of time zones, we can move on and start actually displaying local time. We’ll be storing all of our dates and times as Unix timestamps for this tutorial, and the easiest way to reason with Unix time is to always think of it as being in UTC, as it represents the number of seconds since Jan 1, 1970 at midnight UTC. Things get much simpler if we always store dates as UTC, the way the Unix timestamp inherently represents.
It doesn’t matter how your server is configured, or how PHP or MySQL are configured, PHP’s time() will always return the number of seconds since Jan 1, 1970 at midnight UTC. As an example, in order to mark 2012′s New Year in New York City, we would store the timestamp 1325394000, which represents both 2012-01-01 05:00:00 UTC and 2012-01-01 00:00:00 EST.
Local Time is Presentation
The concept of local time only applies when interfacing with users, making it the concern of the front-end, not the back-end. If we stored our west-coast users’ comment date in PST, and our British users’ comment date in GMT, then we’d have to convert every date whenever we wanted to reason with them. When sorting the comments by date, the business logic would need to know that 3pm PST is actually later than 4pm GMT. Instead, since we’re storing all time zones as UTC (in our example, 11pm UTC and 4pm UTC respectively), the sorting and comparing of dates is unaffected by our desire to maintain user time zones.
The first step, then, is determining how to display local time. PHP gives us a great function called date_default_timezone_set() that could handle all of this magic for us:
<?php
$balldrop = 1325394000; // new years in NYC
$user_timezone = 'PST8PDT'; // as per our west-coast friends' settings
// output local time at the presentation level
date_default_timezone_set($user_timezone);
echo date('r', $balldrop);
// > Sat, 31 Dec 2011 21:00:00 -0800
This gives us the correct response, and behaves exactly as we expect for the most part. In simple examples, small scripts, or single time zone applications, date_default_timezone_set is the correct function to be using.
The limitation of setting a default time zone based on the logged in users’ configuration, however, is that it fails to accommodate different time zones on the same page. If one user’s comment triggers an email notification to the original poster, we would need to include the comment’s time in the recipient’s local time, not the logged in users’ local time.
Consequently, we’re going to need to convert time zones manually before displaying them in order to ensure that we have control over when and which time zones are used.
Converting Dates
PHP provides us with DateTime and DateTimeZone classes to handle most of the heavy lifting with regards to converting between time zones. It also provides a DateTimeZone::getTransitions function to show just how different DST settings are across time zones.
For our purposes, we can just wrap a simple DateTime object to seamlessly convert to local time. This LocalDate function is a drop-in replacement for PHP’s built-in date function, except that it takes an optional time zone for presentation purposes:
<?php
// drop-in replacement for date() that handles local time
function LocalDate($format, $time = null, $timezone = 'UTC')
{
if (is_null($time))
$time = time();
$local = new DateTime('@'.$time);
$local->setTimeZone(new DateTimeZone($timezone));
return $local->format($format);
}
// example:
$balldrop = 1325394000; // new years in NYC
$user_timezone = 'PST8PDT'; // as per our west-coast friends' settings
echo LocalDate('r', $balldrop, $user_timezone);
// > Sat, 31 Dec 2011 21:00:00 -0800
The DateTime function knows that the Unix timestamp is coming in as UTC, and when provided with a time zone adjusts its presentation accordingly. At this stage, we’ve accomplished what we set out to achieve: formatting dates in local time in the front-end. If you’re using a setup that has a User model of some sort, you could write a function to allow you to write $user->localTime('r', $balldrop); and subsequently Auth::$user->localTime('r', $balldrop); for logged in users to automatically use and abstract away the user’s time zone.
User-Specified Dates
The last step to managing time zones is to handle times that originate with users. If you let a user add an event and specify the date and time with an input field (and maybe a jQuery calendar), chances are you’re using strtotime to store it in the database.
As with date, this LocalStrtotime function is a drop-in replacement for strtotime that takes a time zone:
<?php
// drop-in replacement for strtotime() that handles local time
function LocalStrtotime($str, $now = null, $timezone = 'UTC')
{
$localtime = strtotime($str, $now);
$local = new DateTime('@'.$localtime);
$local->setTimeZone(new DateTimeZone($timezone));
// compensate for the the server's time zone
$server = new DateTime('@'.$localtime);
$server->setTimeZone(new DateTimeZone(date_default_timezone_get()));
// manually adjust the timestamp accordingly
return $localtime - $local->getOffset() + $server->getOffset();
}
// example:
$user_timezone = 'America/New_York';
echo LocalStrtotime('Jan 1, 2012', null, 'America/New_York');
// > 1325394000
As with Localdate, we construct a DateTime object with the relevant time, but here we only use this object to obtain the offset from UTC. We also have to account for the server’s default time zone; alternately we could just use date_default_timezone_set('UTC');, but compensating for the difference here ensures portability. As of PHP 5.3.6, it looks like DateTime::modify could be used in place of strtotime, which would simplify this a bit, but the rest of this code should work in PHP 5.2 or greater, so I left it like this.
Relative Time
Of course, aside from all of this, using relative times can sometimes trump local time in situations where exact times need not be referenced. Since all of our dates are stored in UTC, and time() always gives us UTC, calculating the time since or until a given timestamp does not require any understanding of time zones:
<?php
// calculate the amount of time that's passed since the given timestamp
function TimeAgo($time)
{
$periods = array('second', 'minute', 'hour', 'day',
'week', 'month', 'year', 'decade');
$lengths = array(60, 60, 24, 7, 4.35, 12, 10);
$difference = abs(time() - $time);
for($j = 0; $j < count($lengths) && $difference >= $lengths[$j]; $j++)
$difference /= $lengths[$j];
$difference = round($difference);
return ($time > time() ? 'in ' : '')
.$difference.' '.$periods[$j].($difference != 1 ? 's' : '')
.($time > time() ? '' : ' ago');
}
echo TimeAgo(1325394000);
// > 2 months ago
Here, we can determine the relative time (e.g. “in 3 weeks”, or “2 days ago”) with ease and forego any time zone confusion, either technical or social, in the process.
Conclusion
To summarize what was covered in this tutorial:
- Asking the user for their time zone is sometimes the hardest part. Determine an acceptable compromise between accuracy and user experience based on the importance of local time in your application.
- The fundamental law of time zone handling is that all dates and times should be stored as Unix timestamps in UTC in the database.
- Showing local time to users belongs in the presentation layer; use
LocalDateas a drop-in replacement fordateto show local time. - Convert user-specified local times to UTC before storing them using
LocalStrtotimeas a drop-in replacement forstrtotime. - If you have a User model,
User::localDate()andUser::localStrtotime()make handling time zones much easier. Storing the user’s preferred date format along with the time zone (for internationalization amongst other things) can make this even more convenient. - When exact dates and times are not needed, relative times can provide a better user experience and can be handled without worrying about time zone issues since dates are stored in UTC.
This should hopefully cover the core aspects of handling user-specified time zones and help get things rolling in the right direction for developers looking to provide local time to their users. If you have any questions, please ask, I’d be happy to help.
One Response to How to Handle Time Zones in PHP
Terry March 9, 2012
Very good article! We are linking to this great article on our site.
Keep up the good writing.