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:
- 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
)
- 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.