Geekery

If you’ve ever had to deal with time and PHP before, chances are you’ve had to deal with time zones. Dealing with time is difficult enough, but once the time becomes variable based on the preferences of the user of your application, it becomes even more confusing.

Why so confusing? I mean, it seems simple enough doesn’t it?

Well, there’s a lot to consider:

  • How do you store the dates?
  • How do you store the time zones?
  • How do you do the math for adjustments?
  • etc.

Through enough research and googling, you can answer most of these questions. However, there came a time to decide how the time zone was going to be displayed to the user. In PHP you have some built in options that are fairly handy. However, we wanted to show the user the time zone abbreviation. In addition, we had to match up what PHP does with the time zone options that we give our users. The time zone options are stored in our database like so:

 timezone_id |                   name              | gmt_offset
-------------+-------------------------------------+------------
           1 | -12:00 International Date Line W.   |       -720
           2 | -11:00 Midway Island, Samoa         |       -660
           3 | -10:00 Hawaii                       |       -600
...

Pretty straightforward list of time zones with their IDs along with the offset in minutes. This does not include every single time zone under the sun mind you.

The PHP functionality provides a fairly easy way to find the different time zone codes in the country/area format such as America/Los_Angeles or Europe/Berlin. You can use functions such as timezone_abbreviations_list to get the offset along with the time zone code or you can use timezone_identifiers_list to get a list of just the time zone codes.

However, PHP does not have any simple means of gathering the time zone abbreviations for each time zone such as MDT or BST. This is the format that we wanted to use to display to our client.

So, here's a simple function to list them all out for you:

$time_zones = timezone_identifiers_list();
$time_to_use = 'now'; # just a dummy time
$time_zone_abbreviations = array();
foreach ($time_zones as $time_zone_id) {
    $dateTime = new DateTime($time_to_use);
    $dateTime->setTimeZone(new DateTimeZone($time_zone_id));
    $abbreviation = $dateTime->format('T');
    if (!in_array($abbreviation, $time_zone_abbreviations)) {
        echo $time_zone_id . ' - ' . $abbreviation . "\n";
        $time_zone_abbreviations[] = $abbreviation;
    }
}

This will give you a wonderful list of unique time zone codes that you can use now:

Africa/Abidjan - GMT
Africa/Addis_Ababa - EAT
Africa/Algiers - CET
Africa/Bangui - WAT
etc.

Eventually you may come to an unexpected result in the list:

Factory - Local time zone must be set--se

But, for the most part, this is a useful list.

Now, this list is okay, but what if you need to know the offsets that go with this? Well, PHP provides another function called timezone_abbreviations_list. This gives us the time zone code and the offsets. Cool: let's modify our previous function and make our list that way.

This is where some unexpected results start to occur. The timezone_abbreviations_list function returns an array of time zones that are sorted by their abbreviation like so:

["mst"]=>
  array(32) {
    [0]=>
    array(3) {
      ["dst"]=>
      bool(false)
      ["offset"]=>
      int(-25200)
      ["timezone_id"]=>
      string(14) "America/Denver"
    }
    [1]=>
    array(3) {
      ["dst"]=>
      bool(false)
      ["offset"]=>
      int(-25200)
      ["timezone_id"]=>
      string(13) "America/Boise"
    
    etc...

Our previous function extracted the abbreviation by using the long description, such as America/Boise, to create a date. We are going to do the same thing, only we are going to use the timezone_ids that are given to us from the timezone_abbreviations_list function. It seems like this would be the correct route since we can have access to the offsets from that function. For now, we just want to produce the same list as we did before with this function:

$time_zones = timezone_abbreviations_list();
$time_to_use = 'now'; # just a dummy time
$time_zone_abbreviations = array();
foreach ($time_zones as $key => $time_zone_array) {
    foreach ($time_zone_array as $key => $info) {
        $time_zone_id = $info['timezone_id'];
        $dateTime = new DateTime($time_to_use);
        $dateTime->setTimeZone(new DateTimeZone($time_zone_id));
        $abbreviation = $dateTime->format('T');
        if (!in_array($abbreviation, $time_zone_abbreviations)) {
            echo $time_zone_id . ' - ' . $abbreviation . "\n";
            $time_zone_abbreviations[] = $abbreviation;
        }
    }
}

It all goes fine and dandy until it gets to the end of the list:

...
Antarctica/Vostok - VOST
Pacific/Efate - VUT
America/Godthab - WGST
Asia/Yakutsk - YAKST

Fatal error: Uncaught exception 'Exception' with message 'DateTimeZone::__construct() [function.DateTimeZone---construct]: Unknown or bad timezone ()'

Uh-oh, that doesn't seem good. It looks like it is trying to construct the time with no time zone information. A bit of code will show us where the problem lies:

$time_zones = timezone_abbreviations_list();
foreach ($time_zones as $key => $time_zone_array) {
    var_dump($key, $time_zone_array);
}

We can go through the list of arrays and look to see where the problem is occurring. Eventually we come to listings that look like this:

string(1) "a"
array(1) {
  [0]=>
  array(3) {
    ["dst"]=>
    bool(false)
    ["offset"]=>
    int(3600)
    ["timezone_id"]=>
    NULL
  }
}
string(1) "b"
array(1) {
  [0]=>
  array(3) {
    ["dst"]=>
    bool(false)
    ["offset"]=>
    int(7200)
    ["timezone_id"]=>
    NULL
  }
}

Yes, the entire alphabet is at the end of the list. There are a bunch of arrays with keys of a-z that have null timezone_ids associated with them. What are they there for? I can't find any rhyme or reason to the entries, but they are there, and they mess up our function.

A quick change to our code to check for empty timezone_ids will make our function work properly:

$time_zones = timezone_abbreviations_list();
$time_to_use = 'now'; # just a dummy time
$time_zone_abbreviations = array();
foreach ($time_zones as $key => $time_zone_array) {
    foreach ($time_zone_array as $key => $info) {
        $time_zone_id = $info['timezone_id'];		
        if (!empty($time_zone_id) {
            $dateTime = new DateTime($time_to_use);
            $dateTime->setTimeZone(new DateTimeZone($time_zone_id));
            $abbreviation = $dateTime->format('T');
            if (!in_array($abbreviation, $time_zone_abbreviations)) {
                echo $time_zone_id . ' - ' . $abbreviation . "\n";
                $time_zone_abbreviations[] = $abbreviation;
            }
        }
    }
}

Now we should have the same list we did before. But, we don't. The first script, using the timezone_identifiers_list function gave us 175 results. Our other script? 136. So, what are the time zones that exist in the timezone_identifiers_list function that don't show up in the timezone_abbreviations_list function? Here's the list:

Indian/Cocos - CCT
Indian/Christmas - CXT
Pacific/Tarawa - GILT
Etc/GMT+1 - GMT+1
Etc/GMT+10 - GMT+10
Etc/GMT+11 - GMT+11
Etc/GMT+12 - GMT+12
Etc/GMT+2 - GMT+2
Etc/GMT+3 - GMT+3
Etc/GMT+4 - GMT+4
Etc/GMT+5 - GMT+5
Etc/GMT+6 - GMT+6
Etc/GMT+7 - GMT+7
Etc/GMT+8 - GMT+8
Etc/GMT+9 - GMT+9
Etc/GMT-1 - GMT-1
Etc/GMT-10 - GMT-10
Etc/GMT-11 - GMT-11
Etc/GMT-12 - GMT-12
Etc/GMT-13 - GMT-13
Etc/GMT-14 - GMT-14
Etc/GMT-2 - GMT-2
Etc/GMT-3 - GMT-3
Etc/GMT-4 - GMT-4
Etc/GMT-5 - GMT-5
Etc/GMT-6 - GMT-6
Etc/GMT-7 - GMT-7
Etc/GMT-8 - GMT-8
Etc/GMT-9 - GMT-9
Factory - Local time zone must be set--se
Pacific/Port_Moresby - PGT
Pacific/Ponape - PONT
Pacific/Palau - PWT
Pacific/Fakaofo - TKT
Pacific/Truk - TRUT
Pacific/Funafuti - TVT
Etc/UCT - UCT
Pacific/Wake - WAKT
Pacific/Wallis - WFT

So now we know that we can get a more complete list of the PHP abbreviated time zones by just using the timezone_identifiers_list function. We are going to use that list and instead of relying on the offset given to us by the timezone_abbreviations_list, we are going to use the built-in getOffset function to get the offset from GMT:

$time_zones = timezone_identifiers_list();
$time_to_use = 'now'; # just a dummy time
$time_zone_abbreviations = array();
foreach ($time_zones as $time_zone_id) {
    $dateTime = new DateTime($time_to_use);
    $dateTime->setTimeZone(new DateTimeZone($time_zone_id));
    $abbreviation = $dateTime->format('T');
    $offset = $dateTime->getOffset() / 60;
    echo $offset . ' - ' . $abbreviation . ' (' . $time_zone_id . ')' . "\n";
}

This looks like the useful list that we were looking for in the beginning:

0 - GMT (Africa/Abidjan)
0 - GMT (Africa/Accra)
180 - EAT (Africa/Addis_Ababa)
60 - CET (Africa/Algiers)
...

Now you know all the acceptable time zone abbreviations to use for PHP! But wait, there's a catch (isn't there always?):

Some time zone abbreviations refer to more than one time offset!

Do'h!

Let's modify our function so that we know which ones to be careful for:

$time_zones = timezone_identifiers_list();
$time_to_use = 'now'; # just a dummy time
$time_zone_abbreviations = array();
foreach ($time_zones as $time_zone_id) {
    $dateTime = new DateTime($time_to_use);
    $dateTime->setTimeZone(new DateTimeZone($time_zone_id));
    $abbreviation = $dateTime->format('T');
    $offset = $dateTime->getOffset() / 60;
    if (!array_key_exists($abbreviation, $time_zone_abbreviations)) {
        $time_zone_abbreviations[$abbreviation] = $offset;
    } else {
        if ($time_zone_abbreviations[$abbreviation] != $offset) {
            // We have two offsets for the same abbreviation
            $previous_offset = $time_zone_abbreviations[$abbreviation];
            $same_abb_diff_o[$abbreviation] = $previous_offset . ' and ' . $offset;
        }
    }
    $time_zone_ids[$abbreviation] = $time_zone_id;
}
foreach ($time_zone_abbreviations as $abbreviation => $offset) {
    echo $offset . ' - ' . $abbreviation . ' (' . $time_zone_ids[$abbreviation] . ')' . "\n";
}
echo "Watch out for:\n";
foreach ($same_abb_diff_o as $abbreviation => $info) {
    echo $abbreviation . ' same abbreviation used for ' . $info . "\n";
}

This gives us our same list before, but it lets us know that there are a few to watch our for at the end:

CDT same abbreviation used for -300 and -240
AST same abbreviation used for -240 and 180
ADT same abbreviation used for -180 and 240
CST same abbreviation used for -360 and 480
GST same abbreviation used for 240 and -120
EST same abbreviation used for -300 and 600
IST same abbreviation used for 330 and 60
WST same abbreviation used for 480 and -660

So what did we learn here today?:

  • There are helpful time zone functions available in PHP.
  • When speaking of time zone abbreviations, you may get unexpected results between the timezone_identifiers_list function and the timezone_abbreviations_list functions.
  • The timezone_abbreviations_list function returns an array that (for some odd reason) has time zone entries for each letter of the alphabet, a-z, that have no valid timezone_id.
  • Using the time zone abbreviation when setting time zones is not always safe.

I hope this post was helpful to those struggling to understand the time zone functions or to others who are trying to utilize time zone abbreviations within PHP. Once you understand how the time zone functions work, and how you can get access to the abbreviations, it becomes a bit easier to understand how to shape your code for your application.

If you enjoyed this post, then please be sure to subscribe to my feed.