John Green
John Green

Reputation: 13435

Best way to populate a <SELECT> box with TimeZones

I need to display a timezone selector as a user control, which always seems easier than it really is. Internally, I store everything with a DateTimeZone Identifier, as that seems to be the smartest way to have the level of accuracy I need, as this project bridges real-world times as it is tied to terrestrial media.

What I don't want to do is present a select box with 300+ time zones, nor do I want to create faked timezone offsets with something like 'UTC-8' (which loses not only DST info, but the actual dates that the DST falls on).

In the end, I'll need a select with options containing the proper TZD Identifiers, something like this (the bracketed #s aren't necessary, just for potential end-user illustration):

<select>
<option value="America/Los_Angeles">Los Angeles [UTC-7 | DST]</option>
...
</select>

Does anyone have any pointers for building this list? All of the solutions I've googled have been problematic in one way or another.


I've added a bounty in case that might entice somebody to share a nicer answer with us. : )

Upvotes: 44

Views: 27480

Answers (12)

bur
bur

Reputation: 754

Similar to the accepted answer, but alot shorter, and also shows the current time in each timezone:

foreach(timezone_identifiers_list() as $tz)
{
    $current_dt = new DateTime('now', new DateTimeZone($tz));

    echo '<option value=' . $tz . '>'
            . str_replace('_', ' ', $tz) . ' - ' // Some timezones contain _
            . $current_dt->format('\G\M\TP (H:i)') // Add the time in $tz
        . '</option>';
}

Outputs something like: Europe/Brussels - GMT+02:00 (23:57)
Any valid format (GMT == UTC) can be used for the timezone/current time.

Upvotes: 1

Pedro Lobito
Pedro Lobito

Reputation: 98961

My solution:

To avoid a huge timezone list, have the user select the country first, then use that information to populate a list of timezones.

File populate.php

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
    <head>
        <meta http-equiv="Content-type" content="text/html; charset=utf-8">
        <title>Select test</title>
        <script type="text/javascript" src="http://code.jquery.com/jquery-1.6.2.min.js"></script>
        <script type="text/javascript" charset="utf-8">
        $(function(){
            $("select#country").change(function(){
                $.getJSON("json.php",{country: $(this).val()}, function(j){
                    var options = '';
                    for (var i = 0; i < j.length; i++) {
                        options += '<option value="' + j[i].optionValue + '">' + j[i].optionDisplay + '</option>';
                    }
                    $("#city").html(options);
                    $('#city option:first').attr('selected', 'selected');
                })
            })            
        })
        </script>
    </head>

    <body>

<form action="#">
  <label for="country">Country:</label>
  <select name="country" id="country">
    <option value="Portugal">Portugal</option>
    <option value="United States">United States</option>
    <option value="Japan">Japan</option>
  </select>
  <label for="city">Timezone:</label>
  <select name="city" id="city">
    <option value="Atlantic/Azores">Atlantic/Azores</option>
    <option value="Atlantic/Madeira">Atlantic/Madeira</option>
    <option value="Europe/Lisbon">Europe/Lisbon</option>
  </select>
<input type="submit" name="action" value="Set TZ" />
</form>

file json.php

$country = $_GET['country'];
$citylist = "";
$country_list = file_get_contents("country_iso.txt"); //grab this file @ http://pastebin.com/e8gxcVHm

preg_match_all('/(.*?):'.$country.'/im', $country_list, $country_iso, PREG_PATTERN_ORDER);
$country_iso = $country_iso[1][0];


if(isset($country_iso))
{
$tz = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $country_iso); //php 5.3 needed to use DateTimeZone::PER_COUNTRY !

foreach($tz as $city)   
    $citylist .= "{\"optionValue\": \"$city\", \"optionDisplay\": \"$city\"}, ";   
}

$citylist = preg_replace('/, $/im', '', $citylist);
$citylist = "[".$citylist."]";

echo $citylist; 

I hope it helps you :)

Upvotes: 9

Ryan C
Ryan C

Reputation: 1013

This has long been answered but, I wasn't satisfied with any of the answers that forced users to go through all the TZ entries php knows. Instead I created a more concise list that maps times like eastern time to America/New_York and includes special entries for odd locations like Arizona.

Github entry: https://github.com/ryanzor/timezone-dropdown

Demo of just selector: http://lifesnow.com/time-zone-dropdown/

<?php

/**
  * 
  * This get's the timezone offset based on the olson code.
  * In this code it is used to find the offset between the given olson code and UTC, but can be used to convert other differences
  * 
  * @param string $remote_tz TZ string
  * @param string $origin_tz TZ string, defaults to UTC
  * @return int offset in seconds
  */

 function ln_get_timezone_offset($remote_tz, $origin_tz = 'UTC') {
    $origin_dtz = new DateTimeZone($origin_tz);
    $remote_dtz = new DateTimeZone($remote_tz);
    $origin_dt = new DateTime("now", $origin_dtz);
    $remote_dt = new DateTime("now", $remote_dtz);
    $offset = $remote_dtz->getOffset($remote_dt) - $origin_dtz->getOffset($origin_dt);
    return $offset;
}

/**
 * Converts a timezone difference to be displayed as GMT +/-
 * 
 * @param string $timezone TZ time
 * @return string text with GMT
 */

function ln_get_timezone_offset_text($timezone){
    $time = ln_get_timezone_offset($timezone);

    $minutesOffset = $time/60;
    $hours = floor(($minutesOffset)/60);
    $minutes = abs($minutesOffset%60);
    $minutesFormatted = sprintf('%02d', $minutes);
    $plus = '';
    if($time >= 0){
        $plus = '+';
    }
    $GMToff = 'GMT '.$plus.$hours.':'.$minutesFormatted;
    return $GMToff;
}


/**
 * This is for formatting how the timezone option displays.
 * It can be converted to include current time, not include gmt or anything like that.
 * 
 * @param string $timezone TZ time
 * @param string $text format select box option
 */
function ln_display_timezone_option($timezone, $text){
    ?>
    <option value="<?php echo $timezone; ?>"><?php echo '('.ln_get_timezone_offset_text($timezone).') '.$text; ?></option>
    <?php
}

/**
 *  The concise list of timezones.  This generates the html wherever it is called
 */

function ln_display_timezone_selector(){

    ?>
    <select name="timezoneSelectDropdown">
        <?php
        ln_display_timezone_option('Pacific/Auckland', 'International Date Line West');
        ln_display_timezone_option('Pacific/Midway', 'Midway Island, Samoa');
        ln_display_timezone_option('US/Hawaii', 'Hawaii');
        ln_display_timezone_option('US/Alaska', 'Alaska');
        ln_display_timezone_option('US/Pacific', 'Pacific Time (US & Canada)');
        ln_display_timezone_option('America/Tijuana', 'Tijuana, Baja California');
        ln_display_timezone_option('America/Phoenix', 'Arizona');
        ln_display_timezone_option('America/Chihuahua', 'Chihuahua, La Paz, Mazatlan');
        ln_display_timezone_option('US/Mountain', 'Mountain Time (US & Canada)');
        ln_display_timezone_option('America/Cancun', 'Central America');
        ln_display_timezone_option('US/Central', 'Central Time (US & Canada)');
        ln_display_timezone_option('America/Mexico_City', 'Guadalajara, Mexico City, Monterrey');
        ln_display_timezone_option('Canada/Saskatchewan', 'Saskatchewan');
        ln_display_timezone_option('America/Lima', 'Bogota, Lima, Quito, Rio Branco');
        ln_display_timezone_option('US/Eastern', 'Eastern Time (US & Canada)');
        ln_display_timezone_option('US/East-Indiana', 'Indiana (East)');
        ln_display_timezone_option('Canada/Atlantic', 'Atlantic Time (Canada)');
        ln_display_timezone_option('America/Caracas', 'Caracas, La Paz');
        ln_display_timezone_option('America/Manaus', 'Manaus');
        ln_display_timezone_option('America/Santiago', 'Santiago');
        ln_display_timezone_option('Canada/Newfoundland', 'Newfoundland');
        ln_display_timezone_option('America/Sao_Paulo', 'Brasilia');
        ln_display_timezone_option('America/Argentina/Buenos_Aires', 'Buenos Aires, Georgetown');
        ln_display_timezone_option('America/Godthab', 'Greenland');
        ln_display_timezone_option('America/Montevideo', 'Montevideo');
        ln_display_timezone_option('Atlantic/South_Georgia', 'Mid-Atlantic');
        ln_display_timezone_option('Atlantic/Cape_Verde', 'Cape Verde Is.');
        ln_display_timezone_option('Atlantic/Azores', 'Azores');
        ln_display_timezone_option('Africa/Casablanca', 'Casablanca, Monrovia, Reykjavik');
        ln_display_timezone_option('UTC', 'Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London');
        ln_display_timezone_option('Europe/Amsterdam', 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna');
        ln_display_timezone_option('Europe/Belgrade', 'Belgrade, Bratislava, Budapest, Ljubljana, Prague');
        ln_display_timezone_option('Europe/Brussels', 'Brussels, Copenhagen, Madrid, Paris');
        ln_display_timezone_option('Europe/Sarajevo', 'Sarajevo, Skopje, Warsaw, Zagreb');
        ln_display_timezone_option('Africa/Windhoek', 'West Central Africa');
        ln_display_timezone_option('Asia/Amman', 'Amman');
        ln_display_timezone_option('Europe/Athens', 'Athens, Bucharest, Istanbul');
        ln_display_timezone_option('Asia/Beirut', 'Beirut');
        ln_display_timezone_option('Africa/Cairo', 'Cairo');
        ln_display_timezone_option('Africa/Harare', 'Harare, Pretoria');
        ln_display_timezone_option('Europe/Helsinki', 'Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius');
        ln_display_timezone_option('Asia/Jerusalem', 'Jerusalem');
        ln_display_timezone_option('Europe/Minsk', 'Minsk');
        ln_display_timezone_option('Africa/Windhoek', 'Windhoek');
        ln_display_timezone_option('Asia/Kuwait', 'Kuwait, Riyadh, Baghdad');
        ln_display_timezone_option('Europe/Moscow', 'Moscow, St. Petersburg, Volgograd');
        ln_display_timezone_option('Africa/Nairobi', 'Nairobi');
        ln_display_timezone_option('Asia/Tbilisi', 'Tbilisi');
        ln_display_timezone_option('Asia/Tehran', 'Tehran');
        ln_display_timezone_option('Asia/Muscat', 'Abu Dhabi, Muscat');
        ln_display_timezone_option('Asia/Baku', 'Baku');
        ln_display_timezone_option('Asia/Yerevan', 'Yerevan');
        ln_display_timezone_option('Asia/Kabul', 'Kabul');
        ln_display_timezone_option('Asia/Yekaterinburg', 'Yekaterinburg');
        ln_display_timezone_option('Asia/Karachi', 'Islamabad, Karachi, Tashkent');
        ln_display_timezone_option('Asia/Kolkata', 'Sri Jayawardenepura');
        ln_display_timezone_option('Asia/Kolkata', 'Chennai, Kolkata, Mumbai, New Delhi');
        ln_display_timezone_option('Asia/Kathmandu', 'Kathmandu');
        ln_display_timezone_option('Asia/Almaty', 'Almaty, Novosibirsk');
        ln_display_timezone_option('Asia/Dhaka', 'Astana, Dhaka');
        ln_display_timezone_option('Asia/Rangoon', 'Yangon (Rangoon)');
        ln_display_timezone_option('Asia/Bangkok', 'Bangkok, Hanoi, Jakarta');
        ln_display_timezone_option('Asia/Krasnoyarsk', 'Krasnoyarsk');
        ln_display_timezone_option('Asia/Shanghai', 'Beijing, Chongqing, Hong Kong, Urumqi');
        ln_display_timezone_option('Asia/Singapore', 'Kuala Lumpur, Singapore');
        ln_display_timezone_option('Asia/Irkutsk', 'Irkutsk, Ulaan Bataar');
        ln_display_timezone_option('Australia/Perth', 'Perth');
        ln_display_timezone_option('Asia/Taipei', 'Taipei');
        ln_display_timezone_option('Asia/Tokyo', 'Osaka, Sapporo, Tokyo');
        ln_display_timezone_option('Asia/Seoul', 'Seoul');
        ln_display_timezone_option('Asia/Yakutsk', 'Yakutsk');
        ln_display_timezone_option('Australia/Adelaide', 'Adelaide');
        ln_display_timezone_option('Australia/Darwin', 'Darwin');
        ln_display_timezone_option('Australia/Brisbane', 'Brisbane');
        ln_display_timezone_option('Australia/Sydney', 'Canberra, Melbourne, Sydney');
        ln_display_timezone_option('Australia/Hobart', 'Hobart');
        ln_display_timezone_option('Pacific/Guam', 'Guam, Port Moresby');
        ln_display_timezone_option('Asia/Vladivostok', 'Vladivostok');
        ln_display_timezone_option('Asia/Magadan', 'Magadan, Solomon Is., New Caledonia');
        ln_display_timezone_option('Pacific/Auckland', 'Auckland, Wellington');
        ln_display_timezone_option('Pacific/Fiji', 'Fiji, Kamchatka, Marshall Is.');
        ln_display_timezone_option('Pacific/Tongatapu', 'Nuku\'alofa');

        ?>
    </select>
    <?php
}
?>

Upvotes: 1

Zubair1
Zubair1

Reputation: 2780

function formatOffset($offset) {
        $hours = $offset / 3600;
        $remainder = $offset % 3600;
        $sign = $hours > 0 ? '+' : '-';
        $hour = (int) abs($hours);
        $minutes = (int) abs($remainder / 60);

        if ($hour == 0 AND $minutes == 0) {
            $sign = ' ';
        }
        return $sign . str_pad($hour, 2, '0', STR_PAD_LEFT) .':'. str_pad($minutes,2, '0');

}

$utc = new DateTimeZone('UTC');
$dt = new DateTime('now', $utc);

echo '<select name="userTimeZone">';
foreach(DateTimeZone::listIdentifiers() as $tz) {
    $current_tz = new DateTimeZone($tz);
    $offset =  $current_tz->getOffset($dt);
    $transition =  $current_tz->getTransitions($dt->getTimestamp(), $dt->getTimestamp());
    $abbr = $transition[0]['abbr'];

    echo '<option value="' .$tz. '">' .$tz. ' [' .$abbr. ' '. formatOffset($offset). ']</option>';
}
echo '</select>';

The above will output all of the timezones in select menu with the following format:

<select name="userTimeZone">
<option value="America/Los_Angeles">America/Los_Angeles [PDT -7]</option>
</select>

Upvotes: 70

Alix Axel
Alix Axel

Reputation: 154603

After thinking (and testing) a lot about this problem I've come to the conclusion that you can't use timezone short-lists for the simple fact that timezone rules are dynamic (more than you might expect - 4 timezones will have their rules changed this month) and groups/concatenations/abbreviations are static.

Here is the code for the approach I decided to stick with:

function Timezones($country = null, $continent = null)
{
    $result = array();

    if (is_array($timezones = DateTimeZone::listIdentifiers()) === true)
    {
        $timestamp = strtotime('-6 months', time());

        if ((strlen($country) == 2) && (defined('DateTimeZone::PER_COUNTRY') === true))
        {
            $timezones = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $country);
        }

        foreach (preg_grep('~' . preg_quote($continent, '~') . '/~i', $timezones) as $id)
        {
            $timezone = new DateTimeZone($id);

            if (is_array($transitions = $timezone->getTransitions()) === true)
            {
                while ((isset($result[$id]) !== true) && (is_null($transition = array_shift($transitions)) !== true))
                {
                    $result[$id] = (($transition['isdst'] !== true) && ($transition['ts'] >= $timestamp)) ? $transition['offset'] : null;
                }
            }
        }

        if (array_multisort($result, SORT_NUMERIC, preg_replace('~^[^/]+/~', '', array_keys($result)), SORT_REGULAR, $result) === true)
        {
            foreach ($result as $key => $value)
            {
                $result[$key] = sprintf('(GMT %+03d:%02u) %s', $value / 3600, abs($value) % 3600 / 60, ltrim(strstr($key, '/'), '/'));
            }
        }
    }

    return str_replace(array(' +00:00', '_', '/'), array('', ' ', ' - '), $result);
}
  • supports filtering by country and continent simultaneously (as in US/America or US/Pacific)
  • the standard (raw) offset is dynamically calculated for each timezone
  • timezones are ordered by their offset first and then by their location
  • the timezone identifier is converted into a human readable representation

Demo at http://www.ideone.com/VYWtw and http://jsfiddle.net/hSxa8/embedded/result/.

Upvotes: 2

Alix Axel
Alix Axel

Reputation: 154603

I came up with a dynamic self-updated solution that doesn't require any lookup tables (select demo):

function Timezones()
{
    $result = array();
    $timezones = array();

    // only process geographical timezones
    foreach (preg_grep('~^(?:A(?:frica|merica|ntarctica|rctic|tlantic|sia|ustralia)|Europe|Indian|Pacific)/~', timezone_identifiers_list()) as $timezone)
    {
        if (is_object($timezone = new DateTimeZone($timezone)) === true)
        {
            $id = array();

            // get only the two most distant transitions
            foreach (array_slice($timezone->getTransitions($_SERVER['REQUEST_TIME']), -2) as $transition)
            {
                // dark magic
                $id[] = sprintf('%b|%+d|%u', $transition['isdst'], $transition['offset'], $transition['ts']);
            }

            if (count($id) > 1)
            {
                sort($id, SORT_NUMERIC); // sort by %b (isdst = 0) first, so that we always get the raw offset
            }

            $timezones[implode('|', $id)][] = $timezone->getName();
        }
    }

    if ((is_array($timezones) === true) && (count($timezones) > 0))
    {
        uksort($timezones, function($a, $b) // sort offsets by -, 0, +
        {
            foreach (array('a', 'b') as $key)
            {
                $$key = explode('|', $$key);
            }

            return intval($a[1]) - intval($b[1]);
        });

        foreach ($timezones as $key => $value)
        {
            $zone = reset($value); // first timezone ID is our internal timezone
            $result[$zone] = preg_replace(array('~^.*/([^/]+)$~', '~_~'), array('$1', ' '), $value); // "humanize" city names

            if (array_key_exists(1, $offset = explode('|', $key)) === true) // "humanize" the offset
            {
                $offset = str_replace(' +00:00', '', sprintf('(UTC %+03d:%02u)', $offset[1] / 3600, abs($offset[1]) % 3600 / 60));
            }

            if (asort($result[$zone]) === true) // sort city names
            {
                $result[$zone] = trim(sprintf('%s %s', $offset, implode(', ', $result[$zone])));
            }
        }
    }

    return $result;
}

There are lots of timezones that share the exact same offsets and DST timings (Europe/Dublin, Europe/Lisbon and Europe/London to name a few), my algorithm groups these zones (using a special notation in the array keys dst?|offset|timestamp) in the first timezone ID of that group and concatenates humanized transformations of the last (usually city level) segment of the timezone ID:

Array
(
    [Pacific/Midway] => (UTC -11:00) Midway, Niue, Pago Pago
    [America/Adak] => (UTC -10:00) Adak
    [Pacific/Fakaofo] => (UTC -10:00) Fakaofo, Honolulu, Johnston, Rarotonga, Tahiti
    [Pacific/Marquesas] => (UTC -10:30) Marquesas
    [America/Anchorage] => (UTC -09:00) Anchorage, Juneau, Nome, Sitka, Yakutat
    [Pacific/Gambier] => (UTC -09:00) Gambier
    [America/Dawson] => (UTC -08:00) Dawson, Los Angeles, Tijuana, Vancouver, Whitehorse
    [America/Santa_Isabel] => (UTC -08:00) Santa Isabel
    [America/Metlakatla] => (UTC -08:00) Metlakatla, Pitcairn
    [America/Dawson_Creek] => (UTC -07:00) Dawson Creek, Hermosillo, Phoenix
    [America/Chihuahua] => (UTC -07:00) Chihuahua, Mazatlan
    [America/Boise] => (UTC -07:00) Boise, Cambridge Bay, Denver, Edmonton, Inuvik, Ojinaga, Shiprock, Yellowknife
    [America/Chicago] => (UTC -06:00) Beulah, Center, Chicago, Knox, Matamoros, Menominee, New Salem, Rainy River, Rankin Inlet, Resolute, Tell City, Winnipeg
    [America/Belize] => (UTC -06:00) Belize, Costa Rica, El Salvador, Galapagos, Guatemala, Managua, Regina, Swift Current, Tegucigalpa
    [Pacific/Easter] => (UTC -06:00) Easter
    [America/Bahia_Banderas] => (UTC -06:00) Bahia Banderas, Cancun, Merida, Mexico City, Monterrey
    [America/Detroit] => (UTC -05:00) Detroit, Grand Turk, Indianapolis, Iqaluit, Louisville, Marengo, Monticello, Montreal, Nassau, New York, Nipigon, Pangnirtung, Petersburg, Thunder Bay, Toronto, Vevay, Vincennes, Winamac
    [America/Atikokan] => (UTC -05:00) Atikokan, Bogota, Cayman, Guayaquil, Jamaica, Lima, Panama, Port-au-Prince
    [America/Havana] => (UTC -05:00) Havana
    [America/Caracas] => (UTC -05:30) Caracas
    [America/Glace_Bay] => (UTC -04:00) Bermuda, Glace Bay, Goose Bay, Halifax, Moncton, Thule
    [Atlantic/Stanley] => (UTC -04:00) Stanley
    [America/Santiago] => (UTC -04:00) Palmer, Santiago
    [America/Anguilla] => (UTC -04:00) Anguilla, Antigua, Aruba, Barbados, Blanc-Sablon, Boa Vista, Curacao, Dominica, Eirunepe, Grenada, Guadeloupe, Guyana, Kralendijk, La Paz, Lower Princes, Manaus, Marigot, Martinique, Montserrat, Port of Spain, Porto Velho, Puerto Rico, Rio Branco, Santo Domingo, St Barthelemy, St Kitts, St Lucia, St Thomas, St Vincent, Tortola
    [America/Campo_Grande] => (UTC -04:00) Campo Grande, Cuiaba
    [America/Asuncion] => (UTC -04:00) Asuncion
    [America/St_Johns] => (UTC -04:30) St Johns
    [America/Sao_Paulo] => (UTC -03:00) Sao Paulo
    [America/Araguaina] => (UTC -03:00) Araguaina, Bahia, Belem, Buenos Aires, Catamarca, Cayenne, Cordoba, Fortaleza, Jujuy, La Rioja, Maceio, Mendoza, Paramaribo, Recife, Rio Gallegos, Rothera, Salta, San Juan, Santarem, Tucuman, Ushuaia
    [America/Montevideo] => (UTC -03:00) Montevideo
    [America/Godthab] => (UTC -03:00) Godthab
    [America/Argentina/San_Luis] => (UTC -03:00) San Luis
    [America/Miquelon] => (UTC -03:00) Miquelon
    [America/Noronha] => (UTC -02:00) Noronha, South Georgia
    [Atlantic/Cape_Verde] => (UTC -01:00) Cape Verde
    [America/Scoresbysund] => (UTC -01:00) Azores, Scoresbysund
    [Atlantic/Canary] => (UTC) Canary, Dublin, Faroe, Guernsey, Isle of Man, Jersey, Lisbon, London, Madeira
    [Africa/Abidjan] => (UTC) Abidjan, Accra, Bamako, Banjul, Bissau, Casablanca, Conakry, Dakar, Danmarkshavn, El Aaiun, Freetown, Lome, Monrovia, Nouakchott, Ouagadougou, Reykjavik, Sao Tome, St Helena
    [Africa/Algiers] => (UTC +01:00) Algiers, Bangui, Brazzaville, Douala, Kinshasa, Lagos, Libreville, Luanda, Malabo, Ndjamena, Niamey, Porto-Novo, Tunis
    [Africa/Ceuta] => (UTC +01:00) Amsterdam, Andorra, Belgrade, Berlin, Bratislava, Brussels, Budapest, Ceuta, Copenhagen, Gibraltar, Ljubljana, Longyearbyen, Luxembourg, Madrid, Malta, Monaco, Oslo, Paris, Podgorica, Prague, Rome, San Marino, Sarajevo, Skopje, Stockholm, Tirane, Vaduz, Vatican, Vienna, Warsaw, Zagreb, Zurich
    [Africa/Windhoek] => (UTC +01:00) Windhoek
    [Asia/Damascus] => (UTC +02:00) Damascus
    [Asia/Beirut] => (UTC +02:00) Beirut
    [Asia/Jerusalem] => (UTC +02:00) Jerusalem
    [Asia/Nicosia] => (UTC +02:00) Athens, Bucharest, Chisinau, Helsinki, Istanbul, Mariehamn, Nicosia, Riga, Sofia, Tallinn, Vilnius
    [Africa/Blantyre] => (UTC +02:00) Blantyre, Bujumbura, Cairo, Gaborone, Gaza, Harare, Hebron, Johannesburg, Kigali, Lubumbashi, Lusaka, Maputo, Maseru, Mbabane, Tripoli
    [Asia/Amman] => (UTC +02:00) Amman
    [Africa/Addis_Ababa] => (UTC +03:00) Addis Ababa, Aden, Antananarivo, Asmara, Baghdad, Bahrain, Comoro, Dar es Salaam, Djibouti, Juba, Kaliningrad, Kampala, Khartoum, Kiev, Kuwait, Mayotte, Minsk, Mogadishu, Nairobi, Qatar, Riyadh, Simferopol, Syowa, Uzhgorod, Zaporozhye
    [Asia/Tehran] => (UTC +03:30) Tehran
    [Asia/Yerevan] => (UTC +04:00) Yerevan
    [Asia/Dubai] => (UTC +04:00) Dubai, Mahe, Mauritius, Moscow, Muscat, Reunion, Samara, Tbilisi, Volgograd
    [Asia/Baku] => (UTC +04:00) Baku
    [Asia/Kabul] => (UTC +04:30) Kabul
    [Antarctica/Mawson] => (UTC +05:00) Aqtau, Aqtobe, Ashgabat, Dushanbe, Karachi, Kerguelen, Maldives, Mawson, Oral, Samarkand, Tashkent
    [Asia/Colombo] => (UTC +05:30) Colombo, Kolkata
    [Asia/Kathmandu] => (UTC +05:45) Kathmandu
    [Antarctica/Vostok] => (UTC +06:00) Almaty, Bishkek, Chagos, Dhaka, Qyzylorda, Thimphu, Vostok, Yekaterinburg
    [Asia/Rangoon] => (UTC +06:30) Cocos, Rangoon
    [Antarctica/Davis] => (UTC +07:00) Bangkok, Christmas, Davis, Ho Chi Minh, Hovd, Jakarta, Novokuznetsk, Novosibirsk, Omsk, Phnom Penh, Pontianak, Vientiane
    [Antarctica/Casey] => (UTC +08:00) Brunei, Casey, Choibalsan, Chongqing, Harbin, Hong Kong, Kashgar, Krasnoyarsk, Kuala Lumpur, Kuching, Macau, Makassar, Manila, Perth, Shanghai, Singapore, Taipei, Ulaanbaatar, Urumqi
    [Australia/Eucla] => (UTC +08:45) Eucla
    [Asia/Dili] => (UTC +09:00) Dili, Irkutsk, Jayapura, Palau, Pyongyang, Seoul, Tokyo
    [Australia/Adelaide] => (UTC +09:30) Adelaide, Broken Hill
    [Australia/Darwin] => (UTC +09:30) Darwin
    [Antarctica/DumontDUrville] => (UTC +10:00) Brisbane, Chuuk, DumontDUrville, Guam, Lindeman, Port Moresby, Saipan, Yakutsk
    [Australia/Currie] => (UTC +10:00) Currie, Hobart, Melbourne, Sydney
    [Australia/Lord_Howe] => (UTC +10:30) Lord Howe
    [Antarctica/Macquarie] => (UTC +11:00) Efate, Guadalcanal, Kosrae, Macquarie, Noumea, Pohnpei, Sakhalin, Vladivostok
    [Pacific/Norfolk] => (UTC +11:30) Norfolk
    [Antarctica/McMurdo] => (UTC +12:00) Auckland, McMurdo, South Pole
    [Asia/Anadyr] => (UTC +12:00) Anadyr, Fiji, Funafuti, Kamchatka, Kwajalein, Magadan, Majuro, Nauru, Tarawa, Wake, Wallis
    [Pacific/Chatham] => (UTC +12:45) Chatham
    [Pacific/Enderbury] => (UTC +13:00) Enderbury, Tongatapu
    [Pacific/Apia] => (UTC +13:00) Apia
    [Pacific/Kiritimati] => (UTC +14:00) Kiritimati
)

Granted, the city concatenation is still pretty damn long but the list of unique (actual) timezones has dropped from 414 (or 415, if we consider the non-geographical UTC) to 75 - which is pretty good IMO and seems to reflect the list of "normalized" timezones Windows uses (also 75).

There are two big problems with this automated approach:

  1. the chosen timezone ID for a group of cities is the first in alphabetic order, this means that for (UTC) Canary, Dublin, Faroe, Guernsey, Isle of Man, Jersey, Lisbon, London, Madeira the timezone value will be Atlantic/Canary - while there shouldn't be anything wrong with that, it would make more sense to pick a timezone ID associated with a bigger city (like Europe/London)
  2. the concatenation of cities is clearly the biggest problem, there are just too many of them - one way to solve this issue would be by using array_slice($cities, 0, $maxCities) before imploding but this wouldn't have the city dimension into account, and for a limit of 4 Canary, Dublin, Faroe, Guernsey, Isle of Man, Jersey, Lisbon, London, Madeira would become Canary, Dublin, Faroe, Guernsey instead of the more logical Windows equivalent Dublin, Edinburgh, Lisbon, London.

This shouldn't be very useful as it it, but I thought I'd share - perhaps someone else can improve it.

Upvotes: 6

Shane O&#39;Grady
Shane O&#39;Grady

Reputation: 2495

There is a list/reference provided in the answer to this question. It comes up with just 88 unique timezones that would make up your select list.

Upvotes: 1

John Green
John Green

Reputation: 13435

At the end of the day, I don't think there is a good solution for pre-culling the too-large list of Timezones... And the issues involved have made me decide to just start out with a decently-sized class. I'm posting it here as there were some people who were interested in it. Hopefully my solution will help somebody else out too.

What it does:

  • Lets you create a list of timezones from the entire list inside of PHP

  • Lets you create a nice list from your own pre-baked definitions, calcuating current UTC offsets.

  • Lets you create a list by country.

  • De-duplicates timezones with the same abbreviation. Note that I haven't done much research to see if there are duplicates in the list that I shouldn't be getting rid of. It is smart enough to know that even though two timezones may report the same abbreviation (say, MST for Arizona), that it will further key in on whether or not the timezone supports DST any time of the year.

  • Outputs either fairly configurable HTML (without going down into the template route) or JSON for Ajax or inline JavaScript.

  • Calculates the current timezone offset for any given timezone. Note that this isn't static... it will change throughout the year.

  • Provides large amounts of sugar for real-world usage.

What it doesn't do:

  • Separate countries or continents out through an OptGroup. Since I'm already doing a sort by UTC offset, this seemed like it was going to cause more confusion than it was going to alleviate.

Things I might add one day:

  • Build the HTML out of template instead of inline like this.

  • Additional selectors, like taking an array of countries instead of a single one.

  • Grouping, but as I mention above, I'm down on doing that.

  • Code is pretty wet. There are some minor coding style discrepancies.

  • Ability to specify a 'preferred' list of friendly names. For instance, although 'Dawson' is a perfectly viable candidate to be listed as 'the' city for PDT, since it is part of the PST main group and observes Daylight Savings time... a city of just over 1000 residents shouldn't beat Los Angeles, San Francisco, Seattle, or Vancouver just because it shows up first lexically.

  • Raw output, so that nobody is tied to the simple 'toSelect' and 'toJson' methods.

In general, it should suit my needs as is. It properly handles all of the timezones I'm familiar with, and all of the ones that I need for my current project. Unfortunately, my scope of knowledge centers mostly around the US & Western Europe.

Is it perfect? Probably not. Happy to hear about any issues/bugs/improvements anybody might have with this code, as it is likely to wind up in my permanent library. If anybody thinks I'm high... let me know that too. That was the whole point of this question was to find the best way to represent a timezone selector accurately that is actually somewhat useful to end users.

TimezoneList.php:

<?php
class TimezoneList
{
    public $timezones = array();

    private $_initialized = false;
    private $_dt_now;
    private $_utc;


    function __construct($grouped = false)
    {
        $this->_utc = new DateTimeZone('UTC');
        $this->_dt_now = new DateTime('now', $this->_utc);
    }

    // Public Static Alternate Constructors
    public static function byCountry($countryKey)
    {
        $retVal = new TimezoneList();
        $retVal->_setList($countryKey);
        return $retVal;     
    }

    public static function fromTimezones(Array $tzArr)
    {
        $retVal = new TimezoneList();
        foreach ($tzArr as $tzItem)
        {
            $retVal->timezones[] = $tzItem;
        }
        $retVal->_initialized = true;
        return $retVal;
    }

    public static function fromIdentifierList($timezoneList, $friendlyNames = NULL)
    {
        $retVal = new TimezoneList();

        if ($friendlyNames)
        {
            if (count($timezoneList) != count($friendlyNames)) throw new Exception('Array count mismatch in TimezoneBuilder::fromList');
        }

        // I'd normally use a foreach pattern, but since friendlyNames is optional, this seemed the way to go.
        for ($ii = 0; $ii < count($timezoneList); $ii++)
        {   
            $pTimezoneEx = new TimezoneExtended($timezoneList[$ii]);
            if ($friendlyNames)
            {
                $pTimezoneEx->friendlyName = $friendlyNames[$ii];
            }
            $retVal->timezones[] = $pTimezoneEx;
        }

        $retVal->_initialized = true;
        return $retVal;
    }

    // Private Statics
    // Private utility function [ Thanks to Zubair1 ]
    private static function _formatOffset($offset) 
    {
        $hours = $offset / 3600;
        $remainder = $offset % 3600;
        $sign = $hours > 0 ? '+' : '-';
        $hour = (int)abs($hours);
        $minutes = (int)abs($remainder / 60);

        $sign = (($hour == 0) && ($minutes == 0)) ? ' ' : $sign;
        return $sign.str_pad($hour, 2, '0', STR_PAD_LEFT).':'.str_pad($minutes,2, '0');
    }

    // Publics
    public function getUniqueTimezoneList($countryKey = null)
    {
        $this->_initialize();   

        $outArr = array();
        $usedTzs = array();

        foreach ($this->timezones as $timezoneEx)
        {
            if (!(in_array($timezoneEx->currentKey, $usedTzs)))
            {
                $usedTzs[] = $timezoneEx->currentKey;
                $outArr[] = $timezoneEx;
            }
        }
        usort($outArr,array('self','_orderByOffset'));

        return self::fromTimezones($outArr);
    }

    // In final code, I'll use a PHP include with output buffering as a template.
    public function toSelect($displayOffset = true, $displayCurrent = true, $selected = array(), Array $options = array())
    {
        $pOpts = array();
        $pItems = array();
        foreach ($options as $key=>$option)
        {
            $pOpts[] = ' '.$key.'="'.$option.'"';
        }
        if (!is_array($selected)) $selected = array($selected); 

        $outVal = '<select'.implode('', $pOpts).'>'."\n";

        foreach ($this->timezones as $timezoneEx)
        {
            $offset = '';
            $selectionAttr = '';
            if (in_array($timezoneEx->tzkey, $selected))
            {
                $selectionAttr = ' selected="selected"';
            }
            if ($displayOffset)
            {
                $offset = ' ['.$timezoneEx->currentAbbr.' '.self::_formatOffset($timezoneEx->currentOffset);
                if ($displayCurrent && (!($timezoneEx->observesDst))) $offset .= ' ( Does not observe DST ) ';
                $offset .= ']';
            }

            $pItems[] = "\t".'<option value="'.$timezoneEx->tzkey.'"'.$selectionAttr.'>'.$timezoneEx->friendlyName.$offset.'</option>';         
        }
        $outVal .= implode("\n", $pItems)."\n".'</select>';
        return $outVal;
    }
    public function toJson()
    {
        $outArr = array();
        foreach ($this->timezones as $timezoneEx)
        {
            $outArr[] = $timezoneEx->toShallowArray();
        }
        return json_encode($outArr);
    }

    // Privates
    private function _initialize()
    {
        if ($this->_initialized) return;
        $this->_setList();
    }
    private function _orderByOffset($a, $b)
    {
        if(  $a->currentOffset ==  $b->currentOffset ){ return 0 ; } 
        return ($a->currentOffset < $b->currentOffset) ? -1 : 1;
    }
    private function _setList($countryKey = NULL)
    {
        $this->timezones = array();
        $listType = ($countryKey) ? DateTimeZone::PER_COUNTRY : DateTimeZone::ALL;
        $tzIds = DateTimeZone::listIdentifiers($listType, $countryKey);

        foreach ($tzIds as $tzIdentifier)
        {
            $this->timezones[] = new TimezoneExtended($tzIdentifier);
        }
        $this->_initialized = true;
    }   
}

class TimezoneExtended
{
    const START_YEAR = 'January 1st';
    const MID_YEAR = 'July 1st';

    private static $_dt_startYear = NULL; // Static so that we don't have to rebuild it each time we go through.
    private static $_dt_midYear = NULL; 
    private static $_dtz_utc = NULL; 
    private static $_dt_now = NULL; 

    private $_baseObj;
    public $tzkey;
    public $friendlyName;
    public $currentKey;                 // Current Key contains the friendly Timezone Key + whether this timezone observes DST.  
                                        // This is unique across the US & Canada.  Unsure if it will be unique across other Timezones.
    public $currentAbbr;
    public $currentOffset;
    public $currentlyDst;
    public $observesDst     =   false;  // Defaults to off


    function __construct($tzKey)
    {
        if (empty(self::$_dtz_utc)) self::$_dtz_utc = new DateTimeZone('UTC');
        if (empty(self::$_dtz_now)) self::$_dt_now = new DateTime('now', self::$_dtz_utc);

        if (empty(self::$_dt_startYear)) self::$_dt_startYear = new DateTime(self::START_YEAR, self::$_dtz_utc);
        if (empty(self::$_dt_midYear)) self::$_dt_midYear = new DateTime(self::MID_YEAR, self::$_dtz_utc);

        $this->tzkey = $tzKey;
        $this->_baseObj = new DateTimeZone($tzKey);
        if ($this->_baseObj == NULL) throw new Exception('Invalid Timezone Key');

        foreach ($this->_baseObj->getTransitions(self::$_dt_startYear->getTimestamp()) as $transition)
        {
            if ($transition['isdst']) $this->observesDst = true;
        }
        foreach ($this->_baseObj->getTransitions(self::$_dt_midYear->getTimestamp()) as $transition)
        {
            if ($transition['isdst']) $this->observesDst = true;
        }
        $this->friendlyName =str_replace('_',' ',array_pop(explode('/',$tzKey)));
        $pTransition = $this->_baseObj->getTransitions(self::$_dt_now->getTimestamp());
        $this->currentAbbr = $pTransition[0]['abbr']; // With a Timestamp, we should only get one transition.
        $this->currentlyDst = $pTransition[0]['isdst'];
        $this->currentKey = $this->currentAbbr.'_'.$this->observesDst;
        $this->currentOffset = $this->_baseObj->getOffset(self::$_dt_now);
    }
    public function toShallowArray()
    {
        $outArr = array(
            'tzkey'=>$this->tzkey,
            'friendlyName'=>$this->friendlyName,
            'currentOffset'=>$this->currentOffset/3600,
            'observesDst'=>$this->observesDst,
            'currentlyDst'=>$this->currentlyDst,
                    'currentAbbr'=>$this->currentAbbr
        );
        return $outArr;
    }
}

Whew. Here's usage examples (timezones.php):

<?php
include_once 'TimezoneList.php';

/* Example 1:  Get Select Box by Country Code */
$tzl = TimezoneList::byCountry('US');
$tzl = $tzl->getUniqueTimezoneList();
echo $tzl->toSelect(true,true,'America/Los_Angeles');

echo "\n".'<br />'."\n";

/* Example 2:  Get a list by country code, output as JSON for AJAX (or similar uses) */
$_REQUEST['country_code'] = 'US'; // Hack for quick usage.
$tzl_ajax = TimezoneList::byCountry($_REQUEST['country_code']);
$tzl_ajax = $tzl_ajax->getUniqueTimezoneList();
echo '<script type="text/javascript">'."\n";
echo 'var foo = '.$tzl_ajax->toJson().';';
echo "\n".'</script>';  

echo "\n".'<br />'."\n";

/* Example 3:  Get Select Box from a list of TZDs + friendly names */
$tzl2 = TimezoneList::fromIdentifierList(
    array('America/Los_Angeles','America/Boise','America/Phoenix','America/Chicago','America/New_York'),
    array('Pacific','Mountain','Mountain (Arizona)','Central','Eastern')
);
// Example shows setting extra properties on the <SELECT>.
echo $tzl2->toSelect(true,false,'America/Los_Angeles', 
    array('style'=>'font-size:15px; border:1px solid #ccc; padding:4px', 'id'=>'timezone_list', 'class'=>'standard-list', 'name'=>'timezone')
);

echo "\n".'<br />'."\n";    

/* Example 4:  Get a raw list of timezones */
$tzl3 = new TimezoneList(true);
$tzl3 = $tzl3->getUniqueTimezoneList();
echo $tzl3->toSelect(true,false,'America/Los_Angeles');

And here's the output code from the examples in timezones.php:

<select> 
    <option value="Pacific/Honolulu">Honolulu [HST -10:00 ( Does not observe DST ) ]</option> 
    <option value="America/Adak">Adak [HADT -09:00]</option> 
    <option value="America/Anchorage">Anchorage [AKDT -08:00]</option> 
    <option value="America/Phoenix">Phoenix [MST -07:00 ( Does not observe DST ) ]</option> 
    <option value="America/Los_Angeles" selected="selected">Los Angeles [PDT -07:00]</option> 
    <option value="America/Boise">Boise [MDT -06:00]</option> 
    <option value="America/Chicago">Chicago [CDT -05:00]</option> 
    <option value="America/Detroit">Detroit [EDT -04:00]</option> 
</select> 
<br /> 
<script type="text/javascript"> 
var foo = [{"tzkey":"Pacific\/Honolulu","friendlyName":"Honolulu","currentOffset":-10,"observesDst":false,"currentlyDst":false,"currentAbbr":"HST"},{"tzkey":"America\/Adak","friendlyName":"Adak","currentOffset":-9,"observesDst":true,"currentlyDst":true,"currentAbbr":"HADT"},{"tzkey":"America\/Anchorage","friendlyName":"Anchorage","currentOffset":-8,"observesDst":true,"currentlyDst":true,"currentAbbr":"AKDT"},{"tzkey":"America\/Phoenix","friendlyName":"Phoenix","currentOffset":-7,"observesDst":false,"currentlyDst":false,"currentAbbr":"MST"},{"tzkey":"America\/Los_Angeles","friendlyName":"Los Angeles","currentOffset":-7,"observesDst":true,"currentlyDst":true,"currentAbbr":"PDT"},{"tzkey":"America\/Boise","friendlyName":"Boise","currentOffset":-6,"observesDst":true,"currentlyDst":true,"currentAbbr":"MDT"},{"tzkey":"America\/Chicago","friendlyName":"Chicago","currentOffset":-5,"observesDst":true,"currentlyDst":true,"currentAbbr":"CDT"},{"tzkey":"America\/Detroit","friendlyName":"Detroit","currentOffset":-4,"observesDst":true,"currentlyDst":true,"currentAbbr":"EDT"}];
</script> 
<br /> 
<select style="font-size:15px; border:1px solid #ccc; padding:4px" id="timezone_list" class="standard-list" name="timezone"> 
    <option value="America/Los_Angeles" selected="selected">Pacific [PDT -07:00]</option> 
    <option value="America/Boise">Mountain [MDT -06:00]</option> 
    <option value="America/Phoenix">Mountain (Arizona) [MST -07:00]</option> 
    <option value="America/Chicago">Central [CDT -05:00]</option> 
    <option value="America/New_York">Eastern [EDT -04:00]</option> 
</select> 
<br /> 
<select> 
    <option value="Pacific/Midway">Midway [SST -11:00]</option> 
    <option value="Pacific/Niue">Niue [NUT -11:00]</option> 
    <option value="Pacific/Apia">Apia [WST -11:00]</option> 
    <option value="Pacific/Tahiti">Tahiti [TAHT -10:00]</option> 
    <option value="Pacific/Honolulu">Honolulu [HST -10:00]</option> 
    <option value="Pacific/Rarotonga">Rarotonga [CKT -10:00]</option> 
    <option value="Pacific/Fakaofo">Fakaofo [TKT -10:00]</option> 
    <option value="Pacific/Marquesas">Marquesas [MART -09:30]</option> 
    <option value="America/Adak">Adak [HADT -09:00]</option> 
    <option value="Pacific/Gambier">Gambier [GAMT -09:00]</option> 
    <option value="America/Anchorage">Anchorage [AKDT -08:00]</option> 
    <option value="Pacific/Pitcairn">Pitcairn [PST -08:00]</option> 
    <option value="America/Dawson_Creek">Dawson Creek [MST -07:00]</option> 
    <option value="America/Dawson">Dawson [PDT -07:00]</option> 
    <option value="America/Belize">Belize [CST -06:00]</option> 
    <option value="America/Boise">Boise [MDT -06:00]</option> 
    <option value="Pacific/Easter">Easter [EAST -06:00]</option> 
    <option value="Pacific/Galapagos">Galapagos [GALT -06:00]</option> 
    <option value="America/Resolute">Resolute [CDT -05:00]</option> 
    <option value="America/Cancun">Cancun [CDT -05:00]</option> 
    <option value="America/Guayaquil">Guayaquil [ECT -05:00]</option> 
    <option value="America/Lima">Lima [PET -05:00]</option> 
    <option value="America/Bogota">Bogota [COT -05:00]</option> 
    <option value="America/Atikokan">Atikokan [EST -05:00]</option> 
    <option value="America/Caracas">Caracas [VET -04:30]</option> 
    <option value="America/Guyana">Guyana [GYT -04:00]</option> 
    <option value="America/Campo_Grande">Campo Grande [AMT -04:00]</option> 
    <option value="America/La_Paz">La Paz [BOT -04:00]</option> 
    <option value="America/Anguilla">Anguilla [AST -04:00]</option> 
    <option value="Atlantic/Stanley">Stanley [FKT -04:00]</option> 
    <option value="America/Detroit">Detroit [EDT -04:00]</option> 
    <option value="America/Boa_Vista">Boa Vista [AMT -04:00]</option> 
    <option value="America/Santiago">Santiago [CLT -04:00]</option> 
    <option value="America/Asuncion">Asuncion [PYT -04:00]</option> 
    <option value="Antarctica/Rothera">Rothera [ROTT -03:00]</option> 
    <option value="America/Paramaribo">Paramaribo [SRT -03:00]</option> 
    <option value="America/Sao_Paulo">Sao Paulo [BRT -03:00]</option> 
    <option value="America/Argentina/Buenos_Aires">Buenos Aires [ART -03:00]</option> 
    <option value="America/Cayenne">Cayenne [GFT -03:00]</option> 
    <option value="America/Glace_Bay">Glace Bay [ADT -03:00]</option> 
    <option value="America/Argentina/San_Luis">San Luis [WARST -03:00]</option> 
    <option value="America/Araguaina">Araguaina [BRT -03:00]</option> 
    <option value="America/Montevideo">Montevideo [UYT -03:00]</option> 
    <option value="America/St_Johns">St Johns [NDT -02:30]</option> 
    <option value="America/Miquelon">Miquelon [PMDT -02:00]</option> 
    <option value="America/Noronha">Noronha [FNT -02:00]</option> 
    <option value="America/Godthab">Godthab [WGST -02:00]</option> 
    <option value="Atlantic/Cape_Verde">Cape Verde [CVT -01:00]</option> 
    <option value="Atlantic/Azores">Azores [AZOST  00:00]</option> 
    <option value="America/Scoresbysund">Scoresbysund [EGST  00:00]</option> 
    <option value="UTC">UTC [UTC  00:00]</option> 
    <option value="Africa/Abidjan">Abidjan [GMT  00:00]</option> 
    <option value="Africa/Casablanca">Casablanca [WET  00:00]</option> 
    <option value="Africa/Bangui">Bangui [WAT +01:00]</option> 
    <option value="Europe/Guernsey">Guernsey [BST +01:00]</option> 
    <option value="Europe/Dublin">Dublin [IST +01:00]</option> 
    <option value="Africa/Algiers">Algiers [CET +01:00]</option> 
    <option value="Atlantic/Canary">Canary [WEST +01:00]</option> 
    <option value="Africa/Windhoek">Windhoek [WAT +01:00]</option> 
    <option value="Africa/Johannesburg">Johannesburg [SAST +02:00]</option> 
    <option value="Africa/Blantyre">Blantyre [CAT +02:00]</option> 
    <option value="Africa/Tripoli">Tripoli [EET +02:00]</option> 
    <option value="Africa/Ceuta">Ceuta [CEST +02:00]</option> 
    <option value="Asia/Jerusalem">Jerusalem [IDT +03:00]</option> 
    <option value="Africa/Addis_Ababa">Addis Ababa [EAT +03:00]</option> 
    <option value="Africa/Cairo">Cairo [EEST +03:00]</option> 
    <option value="Antarctica/Syowa">Syowa [SYOT +03:00]</option> 
    <option value="Europe/Volgograd">Volgograd [VOLST +04:00]</option> 
    <option value="Europe/Samara">Samara [SAMST +04:00]</option> 
    <option value="Asia/Tbilisi">Tbilisi [GET +04:00]</option> 
    <option value="Europe/Moscow">Moscow [MSD +04:00]</option> 
    <option value="Asia/Dubai">Dubai [GST +04:00]</option> 
    <option value="Indian/Mauritius">Mauritius [MUT +04:00]</option> 
    <option value="Indian/Reunion">Reunion [RET +04:00]</option> 
    <option value="Indian/Mahe">Mahe [SCT +04:00]</option> 
    <option value="Asia/Tehran">Tehran [IRDT +04:30]</option> 
    <option value="Asia/Kabul">Kabul [AFT +04:30]</option> 
    <option value="Asia/Aqtau">Aqtau [AQTT +05:00]</option> 
    <option value="Asia/Ashgabat">Ashgabat [TMT +05:00]</option> 
    <option value="Asia/Oral">Oral [ORAT +05:00]</option> 
    <option value="Asia/Yerevan">Yerevan [AMST +05:00]</option> 
    <option value="Asia/Baku">Baku [AZST +05:00]</option> 
    <option value="Indian/Kerguelen">Kerguelen [TFT +05:00]</option> 
    <option value="Indian/Maldives">Maldives [MVT +05:00]</option> 
    <option value="Asia/Karachi">Karachi [PKT +05:00]</option> 
    <option value="Asia/Dushanbe">Dushanbe [TJT +05:00]</option> 
    <option value="Asia/Samarkand">Samarkand [UZT +05:00]</option> 
    <option value="Antarctica/Mawson">Mawson [MAWT +05:00]</option> 
    <option value="Asia/Colombo">Colombo [IST +05:30]</option> 
    <option value="Asia/Kathmandu">Kathmandu [NPT +05:45]</option> 
    <option value="Indian/Chagos">Chagos [IOT +06:00]</option> 
    <option value="Asia/Bishkek">Bishkek [KGT +06:00]</option> 
    <option value="Asia/Almaty">Almaty [ALMT +06:00]</option> 
    <option value="Antarctica/Vostok">Vostok [VOST +06:00]</option> 
    <option value="Asia/Yekaterinburg">Yekaterinburg [YEKST +06:00]</option> 
    <option value="Asia/Dhaka">Dhaka [BDT +06:00]</option> 
    <option value="Asia/Thimphu">Thimphu [BTT +06:00]</option> 
    <option value="Asia/Qyzylorda">Qyzylorda [QYZT +06:00]</option> 
    <option value="Indian/Cocos">Cocos [CCT +06:30]</option> 
    <option value="Asia/Rangoon">Rangoon [MMT +06:30]</option> 
    <option value="Asia/Jakarta">Jakarta [WIT +07:00]</option> 
    <option value="Asia/Hovd">Hovd [HOVT +07:00]</option> 
    <option value="Antarctica/Davis">Davis [DAVT +07:00]</option> 
    <option value="Asia/Bangkok">Bangkok [ICT +07:00]</option> 
    <option value="Indian/Christmas">Christmas [CXT +07:00]</option> 
    <option value="Asia/Omsk">Omsk [OMSST +07:00]</option> 
    <option value="Asia/Novokuznetsk">Novokuznetsk [NOVST +07:00]</option> 
    <option value="Asia/Choibalsan">Choibalsan [CHOT +08:00]</option> 
    <option value="Asia/Ulaanbaatar">Ulaanbaatar [ULAT +08:00]</option> 
    <option value="Asia/Brunei">Brunei [BNT +08:00]</option> 
    <option value="Antarctica/Casey">Casey [WST +08:00]</option> 
    <option value="Asia/Singapore">Singapore [SGT +08:00]</option> 
    <option value="Asia/Manila">Manila [PHT +08:00]</option> 
    <option value="Asia/Hong_Kong">Hong Kong [HKT +08:00]</option> 
    <option value="Asia/Krasnoyarsk">Krasnoyarsk [KRAST +08:00]</option> 
    <option value="Asia/Makassar">Makassar [CIT +08:00]</option> 
    <option value="Asia/Kuala_Lumpur">Kuala Lumpur [MYT +08:00]</option> 
    <option value="Australia/Eucla">Eucla [CWST +08:45]</option> 
    <option value="Pacific/Palau">Palau [PWT +09:00]</option> 
    <option value="Asia/Tokyo">Tokyo [JST +09:00]</option> 
    <option value="Asia/Dili">Dili [TLT +09:00]</option> 
    <option value="Asia/Jayapura">Jayapura [EIT +09:00]</option> 
    <option value="Asia/Pyongyang">Pyongyang [KST +09:00]</option> 
    <option value="Asia/Irkutsk">Irkutsk [IRKST +09:00]</option> 
    <option value="Australia/Adelaide">Adelaide [CST +09:30]</option> 
    <option value="Asia/Yakutsk">Yakutsk [YAKST +10:00]</option> 
    <option value="Australia/Currie">Currie [EST +10:00]</option> 
    <option value="Pacific/Port_Moresby">Port Moresby [PGT +10:00]</option> 
    <option value="Pacific/Guam">Guam [ChST +10:00]</option> 
    <option value="Pacific/Truk">Truk [TRUT +10:00]</option> 
    <option value="Antarctica/DumontDUrville">DumontDUrville [DDUT +10:00]</option> 
    <option value="Australia/Lord_Howe">Lord Howe [LHST +10:30]</option> 
    <option value="Pacific/Ponape">Ponape [PONT +11:00]</option> 
    <option value="Pacific/Kosrae">Kosrae [KOST +11:00]</option> 
    <option value="Antarctica/Macquarie">Macquarie [MIST +11:00]</option> 
    <option value="Pacific/Noumea">Noumea [NCT +11:00]</option> 
    <option value="Pacific/Efate">Efate [VUT +11:00]</option> 
    <option value="Pacific/Guadalcanal">Guadalcanal [SBT +11:00]</option> 
    <option value="Asia/Sakhalin">Sakhalin [SAKST +11:00]</option> 
    <option value="Asia/Vladivostok">Vladivostok [VLAST +11:00]</option> 
    <option value="Pacific/Norfolk">Norfolk [NFT +11:30]</option> 
    <option value="Asia/Kamchatka">Kamchatka [PETST +12:00]</option> 
    <option value="Pacific/Tarawa">Tarawa [GILT +12:00]</option> 
    <option value="Asia/Magadan">Magadan [MAGST +12:00]</option> 
    <option value="Pacific/Wallis">Wallis [WFT +12:00]</option> 
    <option value="Pacific/Kwajalein">Kwajalein [MHT +12:00]</option> 
    <option value="Pacific/Funafuti">Funafuti [TVT +12:00]</option> 
    <option value="Pacific/Nauru">Nauru [NRT +12:00]</option> 
    <option value="Asia/Anadyr">Anadyr [ANAST +12:00]</option> 
    <option value="Antarctica/McMurdo">McMurdo [NZST +12:00]</option> 
    <option value="Pacific/Wake">Wake [WAKT +12:00]</option> 
    <option value="Pacific/Fiji">Fiji [FJT +12:00]</option> 
    <option value="Pacific/Chatham">Chatham [CHAST +12:45]</option> 
    <option value="Pacific/Enderbury">Enderbury [PHOT +13:00]</option> 
    <option value="Pacific/Tongatapu">Tongatapu [TOT +13:00]</option> 
    <option value="Pacific/Kiritimati">Kiritimati [LINT +14:00]</option> 
</select>

Upvotes: 4

F21
F21

Reputation: 33401

Perhaps we could use a pregenerated list like the one presented here: http://www.ultramegatech.com/blog/2009/04/working-with-time-zones-in-php

Upvotes: 3

Anomie
Anomie

Reputation: 94834

If you want to do things with zoneinfo you don't really have any choice but to include hundreds of entries, because that's just the way zoneinfo works. It has generally at least one entry per country, and there are around 200 countries (according to Wikipedia).

What I've done before is to use timezone_identifiers_list() and filter out any entry that isn't in one of the standard regions:

# Output option list, HTML.
$opt = '';

$regions = array('Africa', 'America', 'Antarctica', 'Arctic', 'Asia', 'Atlantic', 'Australia', 'Europe', 'Indian', 'Pacific');
$tzs = timezone_identifiers_list();
$optgroup = '';
sort($tzs);
foreach ($tzs as $tz) {
    $z = explode('/', $tz, 2);
    # timezone_identifiers_list() returns a number of
    # backwards-compatibility entries. This filters them out of the 
    # list presented to the user.
    if (count($z) != 2 || !in_array($z[0], $regions)) continue;
    if ($optgroup != $z[0]) {
        if ($optgroup !== '') $opt .= '</optgroup>';
        $optgroup = $z[0];
        $opt .= '<optgroup label="' . htmlentities($z[0]) . '">';
    }
    $opt .= '<option value="' . htmlentities($tz) . '" label="' . htmlentities(str_replace('_', ' ', $z[1])) . '">' . htmlentities(str_replace('_', ' ', $tz)) . '</option>';
}
if ($optgroup !== '') $opt .= '</optgroup>';

This creates a list with <optgroup> elements, so the list will at least be logically divided by region.

Upvotes: 8

KyleWpppd
KyleWpppd

Reputation: 2030

I can think of a few options.

First being to populate the whole select list, and use a jQuery plugin like Chosen to populate the entire list. This would probably suck since your users might not even know all of the timezones.

Second option being to use JS to get the browser's local time in a hidden input field, and combine that with knowledge of the users IP to try to deduce the users location transparently. You could even display their assumed location on a map and ask them if you have the correct time.

Third option is to actually ask the user where they live. Country and postal code should probably be enough to get the timezone for most users. In edge cases you could ask the user to clarify.

Personally, I'd probably combine the first and second solution to make a select list display the regions you believe the user is closest to.

Upvotes: 3

OverZealous
OverZealous

Reputation: 39570

I found this to be an excellent resource: http://randomdrake.com/2008/08/06/time-zone-abbreviation-difficulties-with-php/

Upvotes: 4

Related Questions