I came up with a dynamic self-updating solution that does not require any lookup tables ( select a 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 many time zones with the same DST offsets and times (for example, Europe/Dublin , Europe/Lisbon and Europe/London ), my algorithm groups these zones (using a special entry in the dst?|offset|timestamp array keys) in the first time zone identifier this group and combines the humanized transformations of the last (usually city level) segment of the time zone identifier:
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 )
Of course, the city’s concatenation is still quite long, but the list of unique (actual) time zones has been reduced from 414 (or 415 if we are considering non-geographic UTC) to 75 — which is pretty good for IMO and seems to reflect a list of Windows using “normalized” time zones (also 75).
This automated approach has two big problems:
- the selected time zone identifier for a group of cities is the first in alphabetical order, which means that for (UTC) Canary, Dublin, Faroe Islands, Guernsey, Isle of Man, Jersey, Lisbon, London, Madeira the time zone value will be
Atlantic/Canary - although in this should not be anything bad, it would be more reasonable to select the time zone identifier associated with the big city (for example, Europe/London ) - city unification is by far the biggest problem, there are too many of them - one way to solve this problem would be to use
array_slice($cities, 0, $maxCities) before deployment, but this would not lead to measuring the city in and for Canary limit 4 , Dublin, Faroe Islands, Guernsey, Isle of Man, Jersey, Lisbon, London, Madeira will become Canaria, Dublin, Faroe Islands , Guernsey instead of the more logical Windows equivalent Dublin, Edinburgh, Lisbon, London ,
This should not be very useful, but I thought that I would share it - maybe someone else could improve it.