Skip to content

Instantly share code, notes, and snippets.

@heathdutton
Created January 28, 2020 20:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save heathdutton/2e229b53ff7d7b737de686056e60eb66 to your computer and use it in GitHub Desktop.
Save heathdutton/2e229b53ff7d7b737de686056e60eb66 to your computer and use it in GitHub Desktop.
Dynamic collection of supported PHP Timezones. Grouped by country, with dynamic abbreviations, DST indicators, and future-proofing. For making useful and clean select2 (or similar) dropdown selectors. Better than all the hard-coded solutions I could find.
<?php
/**
* Class TimeZoneOptionCollection
*
* Dynamic collection of supported PHP Timezones:
* - Grouped by country
* - Commonly used abbreviations
* - DST indicators
* - Future-proof (leans on PHP for timezones).
*
* Laravel example: {{ Form::select('timezone', (new TimeZoneOptionCollection())->toArray(), 'America/New_York', ['class' => 'select2']) }}
*
*/
class TimeZoneOptionCollection
{
/**
* ISO 3166-2 Official updated 2020
* https://datahub.io/core/country-list/r/data.csv
*
* An even more future-proof version would fetch/cache these, but this class is meant to be self-sufficient.
* If a country is not supported, timezones will still be listed and available for selection.
*
* @var array
*/
protected static $countries = [
'Afghanistan' => 'AF',
'Åland Islands' => 'AX',
'Albania' => 'AL',
'Algeria' => 'DZ',
'American Samoa' => 'AS',
'Andorra' => 'AD',
'Angola' => 'AO',
'Anguilla' => 'AI',
'Antarctica' => 'AQ',
'Antigua and Barbuda' => 'AG',
'Argentina' => 'AR',
'Armenia' => 'AM',
'Aruba' => 'AW',
'Australia' => 'AU',
'Austria' => 'AT',
'Azerbaijan' => 'AZ',
'Bahamas' => 'BS',
'Bahrain' => 'BH',
'Bangladesh' => 'BD',
'Barbados' => 'BB',
'Belarus' => 'BY',
'Belgium' => 'BE',
'Belize' => 'BZ',
'Benin' => 'BJ',
'Bermuda' => 'BM',
'Bhutan' => 'BT',
'Bolivia, Plurinational State of' => 'BO',
'Bonaire, Sint Eustatius and Saba' => 'BQ',
'Bosnia and Herzegovina' => 'BA',
'Botswana' => 'BW',
'Bouvet Island' => 'BV',
'Brazil' => 'BR',
'British Indian Ocean Territory' => 'IO',
'Brunei Darussalam' => 'BN',
'Bulgaria' => 'BG',
'Burkina Faso' => 'BF',
'Burundi' => 'BI',
'Cambodia' => 'KH',
'Cameroon' => 'CM',
'Canada' => 'CA',
'Cape Verde' => 'CV',
'Cayman Islands' => 'KY',
'Central African Republic' => 'CF',
'Chad' => 'TD',
'Chile' => 'CL',
'China' => 'CN',
'Christmas Island' => 'CX',
'Cocos (Keeling) Islands' => 'CC',
'Colombia' => 'CO',
'Comoros' => 'KM',
'Congo' => 'CG',
'Congo, the Democratic Republic of the' => 'CD',
'Cook Islands' => 'CK',
'Costa Rica' => 'CR',
'Croatia' => 'HR',
'Cuba' => 'CU',
'Curaçao' => 'CW',
'Cyprus' => 'CY',
'Czech Republic' => 'CZ',
'Côte d\'Ivoire' => 'CI',
'Denmark' => 'DK',
'Djibouti' => 'DJ',
'Dominica' => 'DM',
'Dominican Republic' => 'DO',
'Ecuador' => 'EC',
'Egypt' => 'EG',
'El Salvador' => 'SV',
'Equatorial Guinea' => 'GQ',
'Eritrea' => 'ER',
'Estonia' => 'EE',
'Ethiopia' => 'ET',
'Falkland Islands (Malvinas)' => 'FK',
'Faroe Islands' => 'FO',
'Fiji' => 'FJ',
'Finland' => 'FI',
'France' => 'FR',
'French Guiana' => 'GF',
'French Polynesia' => 'PF',
'French Southern Territories' => 'TF',
'Gabon' => 'GA',
'Gambia' => 'GM',
'Georgia' => 'GE',
'Germany' => 'DE',
'Ghana' => 'GH',
'Gibraltar' => 'GI',
'Greece' => 'GR',
'Greenland' => 'GL',
'Grenada' => 'GD',
'Guadeloupe' => 'GP',
'Guam' => 'GU',
'Guatemala' => 'GT',
'Guernsey' => 'GG',
'Guinea' => 'GN',
'Guinea-Bissau' => 'GW',
'Guyana' => 'GY',
'Haiti' => 'HT',
'Heard Island and McDonald Islands' => 'HM',
'Holy See (Vatican City State)' => 'VA',
'Honduras' => 'HN',
'Hong Kong' => 'HK',
'Hungary' => 'HU',
'Iceland' => 'IS',
'India' => 'IN',
'Indonesia' => 'ID',
'Iran, Islamic Republic of' => 'IR',
'Iraq' => 'IQ',
'Ireland' => 'IE',
'Isle of Man' => 'IM',
'Israel' => 'IL',
'Italy' => 'IT',
'Jamaica' => 'JM',
'Japan' => 'JP',
'Jersey' => 'JE',
'Jordan' => 'JO',
'Kazakhstan' => 'KZ',
'Kenya' => 'KE',
'Kiribati' => 'KI',
'Korea, Democratic People\'s Republic of' => 'KP',
'Korea, Republic of' => 'KR',
'Kuwait' => 'KW',
'Kyrgyzstan' => 'KG',
'Lao People\'s Democratic Republic' => 'LA',
'Latvia' => 'LV',
'Lebanon' => 'LB',
'Lesotho' => 'LS',
'Liberia' => 'LR',
'Libya' => 'LY',
'Liechtenstein' => 'LI',
'Lithuania' => 'LT',
'Luxembourg' => 'LU',
'Macao' => 'MO',
'Macedonia, the Former Yugoslav Republic of' => 'MK',
'Madagascar' => 'MG',
'Malawi' => 'MW',
'Malaysia' => 'MY',
'Maldives' => 'MV',
'Mali' => 'ML',
'Malta' => 'MT',
'Marshall Islands' => 'MH',
'Martinique' => 'MQ',
'Mauritania' => 'MR',
'Mauritius' => 'MU',
'Mayotte' => 'YT',
'Mexico' => 'MX',
'Micronesia, Federated States of' => 'FM',
'Moldova, Republic of' => 'MD',
'Monaco' => 'MC',
'Mongolia' => 'MN',
'Montenegro' => 'ME',
'Montserrat' => 'MS',
'Morocco' => 'MA',
'Mozambique' => 'MZ',
'Myanmar' => 'MM',
'Namibia' => 'NA',
'Nauru' => 'NR',
'Nepal' => 'NP',
'Netherlands' => 'NL',
'New Caledonia' => 'NC',
'New Zealand' => 'NZ',
'Nicaragua' => 'NI',
'Niger' => 'NE',
'Nigeria' => 'NG',
'Niue' => 'NU',
'Norfolk Island' => 'NF',
'Northern Mariana Islands' => 'MP',
'Norway' => 'NO',
'Oman' => 'OM',
'Pakistan' => 'PK',
'Palau' => 'PW',
'Palestine, State of' => 'PS',
'Panama' => 'PA',
'Papua New Guinea' => 'PG',
'Paraguay' => 'PY',
'Peru' => 'PE',
'Philippines' => 'PH',
'Pitcairn' => 'PN',
'Poland' => 'PL',
'Portugal' => 'PT',
'Puerto Rico' => 'PR',
'Qatar' => 'QA',
'Romania' => 'RO',
'Russian Federation' => 'RU',
'Rwanda' => 'RW',
'Réunion' => 'RE',
'Saint Barthélemy' => 'BL',
'Saint Helena, Ascension and Tristan da Cunha' => 'SH',
'Saint Kitts and Nevis' => 'KN',
'Saint Lucia' => 'LC',
'Saint Martin (French part)' => 'MF',
'Saint Pierre and Miquelon' => 'PM',
'Saint Vincent and the Grenadines' => 'VC',
'Samoa' => 'WS',
'San Marino' => 'SM',
'Sao Tome and Principe' => 'ST',
'Saudi Arabia' => 'SA',
'Senegal' => 'SN',
'Serbia' => 'RS',
'Seychelles' => 'SC',
'Sierra Leone' => 'SL',
'Singapore' => 'SG',
'Sint Maarten (Dutch part)' => 'SX',
'Slovakia' => 'SK',
'Slovenia' => 'SI',
'Solomon Islands' => 'SB',
'Somalia' => 'SO',
'South Africa' => 'ZA',
'South Georgia and the South Sandwich Islands' => 'GS',
'South Sudan' => 'SS',
'Spain' => 'ES',
'Sri Lanka' => 'LK',
'Sudan' => 'SD',
'Suriname' => 'SR',
'Svalbard and Jan Mayen' => 'SJ',
'Swaziland' => 'SZ',
'Sweden' => 'SE',
'Switzerland' => 'CH',
'Syrian Arab Republic' => 'SY',
'Taiwan, Province of China' => 'TW',
'Tajikistan' => 'TJ',
'Tanzania, United Republic of' => 'TZ',
'Thailand' => 'TH',
'Timor-Leste' => 'TL',
'Togo' => 'TG',
'Tokelau' => 'TK',
'Tonga' => 'TO',
'Trinidad and Tobago' => 'TT',
'Tunisia' => 'TN',
'Turkey' => 'TR',
'Turkmenistan' => 'TM',
'Turks and Caicos Islands' => 'TC',
'Tuvalu' => 'TV',
'Uganda' => 'UG',
'Ukraine' => 'UA',
'United Arab Emirates' => 'AE',
'United Kingdom' => 'GB',
'United States Minor Outlying Islands' => 'UM',
'United States' => 'US',
'Uruguay' => 'UY',
'Uzbekistan' => 'UZ',
'Vanuatu' => 'VU',
'Venezuela, Bolivarian Republic of' => 'VE',
'Viet Nam' => 'VN',
'Virgin Islands, British' => 'VG',
'Virgin Islands, U.S.' => 'VI',
'Wallis and Futuna' => 'WF',
'Western Sahara' => 'EH',
'Yemen' => 'YE',
'Zambia' => 'ZM',
'Zimbabwe' => 'ZW',
];
/**
* The items contained in the collection.
*
* @var array
*/
protected $items = [];
/**
* TimeZoneOptionCollection constructor.
*
* @param array $items
*
* @throws Exception
*/
public function __construct($items = [])
{
if ($items) {
$this->items = $items;
} else {
$unmapped = array_flip(DateTimeZone::listIdentifiers());
foreach (self::$countries as $country => $iso) {
$identifiers = \DateTimeZone::listIdentifiers(\DateTimeZone::PER_COUNTRY, $iso);
if ($identifiers) {
$country = ucfirst(implode(' ', array_reverse(explode(', ', $country))));
$items[$country] = [];
foreach ($identifiers as $identifier) {
$items[$country][$identifier] = self::identifierToString($identifier);
unset($unmapped[$identifier]);
}
self::ksort($items);
}
}
// Include any unmapped identifiers that do not have a country (future-proofing)
// UTC is not a timezone, but is expected as an option here.
foreach ($unmapped as $identifier => $int) {
$items[$identifier] = self::identifierToString($identifier);
}
self::ksort($items);
$this->items = $items;
}
}
/**
* @param $identifier
*
* @return string
* @throws Exception
*/
private static function identifierToString($identifier)
{
$abbrArr = [];
$time = new \DateTime(null, new \DateTimeZone($identifier));
$abbr = $time->format('T');
$identArr = explode('/', str_replace('_', ' ', $identifier));
$city = end($identArr);
$region = reset($identArr);
$areaArr = array_diff($identArr, [$city, $region]);
// Include region when specified.
if ($areaArr) {
$city = $city.', '.reset($areaArr);
}
// Append common acronym abbreviation if applicable.
if ($abbr && !is_numeric($abbr) && $abbr !== $identifier) {
$abbrArr[] = $abbr;
}
// Append Daylight Savings Time indicator.
if ($time->format('I')) {
$abbrArr[] = 'DST';
}
// Include current GMT offset in simplified notation while still allowing partial-hour timezones.
$abbrArr[] = 'GMT'.str_replace([':00', '-0', '+0', '+0'], ['', '-', '+', ''], $time->format('P'));
return $city.' ('.implode('/', $abbrArr).')';
}
/**
* @param $array
* @param string $in
* @param string $out
*
* @return bool
*/
private static function ksort($array, $in = 'UTF-8', $out = 'ASCII//TRANSLIT//IGNORE')
{
return uksort($array, function ($a, $b) use ($in, $out) {
$a = @iconv($in, $out, $a);
$b = @iconv($in, $out, $b);
return strnatcasecmp($a, $b);
});
}
/**
* {@inheritdoc}
*/
public function get($key)
{
if (null === $key) {
return null;
}
return array_get($this->items, $key);
}
/**
* @param array|string $keys
*
* @return array
*/
public function except($keys)
{
$keys = is_array($keys) ? $keys : func_get_args();
return array_except($this->items, $keys);
}
/**
* @param array|string $keys
*
* @return array
*/
public function only($keys)
{
$keys = is_array($keys) ? $keys : func_get_args();
return array_only($this->items, $keys);
}
/**
* @return array
*/
public function keys()
{
return array_keys($this->items);
}
/**
* {@inheritdoc}
*/
public function toArray()
{
return $this->items;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment