I'll try to explain what's the problem here.
According to list of supported timezones from PHP manual, I can see all valid TZ identifiers in PHP.
My first question is how to get that list from code but that's not what I really need.
My final goal is to write function isValidTimezoneId()
that returns TRUE if timezone is valid, otherwise it should return FALSE.
function isValidTimezoneId($timezoneId) {
# ...function body...
return ?; # TRUE or FALSE
}
So, when I pass TZ identifier using $timezoneId
(string) in function I need boolean result.
Well, what I have so far...
1) Solution using @ operator
First solution I've got is something like this:
function isValidTimezoneId($timezoneId) {
$savedZone = date_default_timezone_get(); # save current zone
$res = $savedZone == $timezoneId; # it's TRUE if param matches current zone
if (!$res) { # 0r...
@date_default_timezone_set($timezoneId); # try to set new timezone
$res = date_default_timezone_get() == $timezoneId; # it's true if new timezone set matches param string.
}
date_default_timezone_set($savedZone); # restore back old timezone
return $res; # set result
}
That works perfectly, but I want another solution (to avoid trying to set wrong timezone)
2) Solution using timezone_identifiers_list()
Then, I was trying to get list of valid timezone identifiers and check it against parameter using in_array() function. So I've tried to use timezone_identifiers_list(), but that was not so good because a lot of timezones was missing in array returned by this function (alias of DateTimeZone::listIdentifiers()). At first sight that was exactly what I was looking for.
function isValidTimezoneId($timezoneId) {
$zoneList = timezone_identifiers_list(); # list of (all) valid timezones
return in_array($timezoneId, $zoneList); # set result
}
This code looks nice and easy but than I've found that $zoneList
array contains ~400 elements. According to my calculations it should return 550+ elements. 150+ elements are missing... So that's not good enough as solution for my problem.
3) Solution based on DateTimeZone::listAbbreviations()
This is last step on my way trying to find perfect solution. Using array returned by this method I can extract all timezone identifiers supported by PHP.
function createTZlist() {
$tza = DateTimeZone::listAbbreviations();
$tzlist = array();
foreach ($tza as $zone)
foreach ($zone as $item)
if (is_string($item['timezone_id']) && $item['timezone_id'] != '')
$tzlist[] = $item['timezone_id'];
$tzlist = array_unique($tzlist);
asort($tzlist);
return array_values($tzlist);
}
This function returns 563 elements (in Example #2
I've got just 407).
I've tried to find differences between those two arrays:
$a1 = timezone_identifiers_list();
$a2 = createTZlist();
print_r(array_values(array_diff($a2, $a1)));
Result is:
Array
(
[0] => Africa/Asmera
[1] => Africa/Timbuktu
[2] => America/Argentina/ComodRivadavia
[3] => America/Atka
[4] => America/Buenos_Aires
[5] => America/Catamarca
[6] => America/Coral_Harbour
[7] => America/Cordoba
[8] => America/Ensenada
[9] => America/Fort_Wayne
[10] => America/Indianapolis
[11] => America/Jujuy
[12] => America/Knox_IN
[13] => America/Louisville
[14] => America/Mendoza
[15] => America/Porto_Acre
[16] => America/Rosario
[17] => America/Virgin
[18] => Asia/Ashkhabad
[19] => Asia/Calcutta
[20] => Asia/Chungking
[21] => Asia/Dacca
[22] => Asia/Istanbul
[23] => Asia/Katmandu
[24] => Asia/Macao
[25] => Asia/Saigon
[26] => Asia/Tel_Aviv
[27] => Asia/Thimbu
[28] => Asia/Ujung_Pandang
[29] => Asia/Ulan_Bator
[30] => Atlantic/Faeroe
[31] => Atlantic/Jan_Mayen
[32] => Australia/ACT
[33] => Australia/Canberra
[34] => Australia/LHI
[35] => Australia/NSW
[36] => Australia/North
[37] => Australia/Queensland
[38] => Australia/South
[39] => Australia/Tasmania
[40] => Australia/Victoria
[41] => Australia/West
[42] => Australia/Yancowinna
[43] => Brazil/Acre
[44] => Brazil/DeNoronha
[45] => Brazil/East
[46] => Brazil/West
[47] => CET
[48] => CST6CDT
[49] => Canada/Atlantic
[50] => Canada/Central
[51] => Canada/East-Saskatchewan
[52] => Canada/Eastern
[53] => Canada/Mountain
[54] => Canada/Newfoundland
[55] => Canada/Pacific
[56] => Canada/Saskatchewan
[57] => Canada/Yukon
[58] => Chile/Continental
[59] => Chile/EasterIsland
[60] => Cuba
[61] => EET
[62] => EST
[63] => EST5EDT
[64] => Egypt
[65] => Eire
[66] => Etc/GMT
[67] => Etc/GMT+0
[68] => Etc/GMT+1
[69] => Etc/GMT+10
[70] => Etc/GMT+11
[71] => Etc/GMT+12
[72] => Etc/GMT+2
[73] => Etc/GMT+3
[74] => Etc/GMT+4
[75] => Etc/GMT+5
[76] => Etc/GMT+6
[77] => Etc/GMT+7
[78] => Etc/GMT+8
[79] => Etc/GMT+9
[80] => Etc/GMT-0
[81] => Etc/GMT-1
[82] => Etc/GMT-10
[83] => Etc/GMT-11
[84] => Etc/GMT-12
[85] => Etc/GMT-13
[86] => Etc/GMT-14
[87] => Etc/GMT-2
[88] => Etc/GMT-3
[89] => Etc/GMT-4
[90] => Etc/GMT-5
[91] => Etc/GMT-6
[92] => Etc/GMT-7
[93] => Etc/GMT-8
[94] => Etc/GMT-9
[95] => Etc/GMT0
[96] => Etc/Greenwich
[97] => Etc/UCT
[98] => Etc/UTC
[99] => Etc/Universal
[100] => Etc/Zulu
[101] => Europe/Belfast
[102] => Europe/Nicosia
[103] => Europe/Tiraspol
[104] => Factory
[105] => GB
[106] => GB-Eire
[107] => GMT
[108] => GMT+0
[109] => GMT-0
[110] => GMT0
[111] => Greenwich
[112] => HST
[113] => Hongkong
[114] => Iceland
[115] => Iran
[116] => Israel
[117] => Jamaica
[118] => Japan
[119] => Kwajalein
[120] => Libya
[121] => MET
[122] => MST
[123] => MST7MDT
[124] => Mexico/BajaNorte
[125] => Mexico/BajaSur
[126] => Mexico/General
[127] => NZ
[128] => NZ-CHAT
[129] => Navajo
[130] => PRC
[131] => PST8PDT
[132] => Pacific/Ponape
[133] => Pacific/Samoa
[134] => Pacific/Truk
[135] => Pacific/Yap
[136] => Poland
[137] => Portugal
[138] => ROC
[139] => ROK
[140] => Singapore
[141] => Turkey
[142] => UCT
[143] => US/Alaska
[144] => US/Aleutian
[145] => US/Arizona
[146] => US/Central
[147] => US/East-Indiana
[148] => US/Eastern
[149] => US/Hawaii
[150] => US/Indiana-Starke
[151] => US/Michigan
[152] => US/Mountain
[153] => US/Pacific
[154] => US/Pacific-New
[155] => US/Samoa
[156] => Universal
[157] => W-SU
[158] => WET
[159] => Zulu
)
This list contains all valid TZ identifiers that Example #2
failed to match.
There's four TZ identifiers more (part of $a1
):
print_r(array_values(array_diff($a1, $a2)));
Output
Array
(
[0] => America/Bahia_Banderas
[1] => Antarctica/Macquarie
[2] => Pacific/Chuuk
[3] => Pacific/Pohnpei
)
So now, I have almost perfect solution...
function isValidTimezoneId($timezoneId) {
$zoneList = createTZlist(); # list of all valid timezones (last 4 are not included)
return in_array($timezoneId, $zoneList); # set result
}
That's my solution and I can use it. Of course, I use this function as part of class so I don't need to generate $zoneList
on every methods call.
What I really need here?
I'm wondering, is there any easier (quicker) solution to get list of all valid timezone identifiers as array (I want to avoid extracting that list from DateTimeZone::listAbbreviations()
if that's possible)? Or if you know another way how to check is timezone parameter valid, please let me know (I repeat, @
operator can't be part of solution).
P.S. If you need more details and examples, let me know. I guess you don't.
I'm using PHP 5.3.5
(think that's not important).
Update
Any part of code that throws exception on invalid timezone string (hidden using @
or caught using try..catch
block) is not solution I'm looking for.
Another update
I've put small bounty on this question!
Now I'm looking for the easiest way how to extract list of all timezone identifiers in PHP array.
Just an addendum to Cal's excellent answer. I think the following might be even faster...
In case of php<5.3, how about this?
You solution works fine, so if it's speed you're looking for I would look more closely at what you're doing with your arrays. I've timed a few thousand trials to get reasonable average times, and these are the results:
Here's the faster function:
The
if
test is faster if you reduce it to justif ($item['timezone_id'])
, but rather than running it 489 times to catch a single case, it's quicker to unset the empty key afterwards.Setting hash keys allows us the skip the
array_unique()
call which is more expensive. Sorting the keys and then extracting them is a tiny bit faster than extracting them and then sorting the extracted list.If you drop the sorting (which is not needed unless you're comparing the list), it gets down to 12,339 microseconds.
But really, you don't need to return the keys anyway. Looking at the holistic
isValidTimezoneId()
, you'd be better off doing this:That is, assuming you only need to test once per execution, otherwise you'd want to save
$valid
after the first run. This approach avoids having to do a sort or converting the keys to values, key lookups are faster than in_array() searches and there's no extra function call. Setting the array values totrue
instead of1
also removes a single cast when the result is true.This brings it reliably down to under 12ms on my test machine, almost half the time of your example. A fun experiment in micro-optimizations!
Why not use @ operator? This code works pretty well, and you don't change default timezone:
If you don't want @, you can do:
I would research what changes the perfect array and use a basic caching mechanism (like store the array in a file, that you include and update when needed). You're currently optimizing building an array that is static for 99.9999% of all the requests.
Edit: Ok, static/dynamic.
Now each time the php version is updated, the file should be regenerated automatically by your code.
When I tried this on a Linux system running 5.3.6, your Example #2 gave me 411 zones and Example #3 gave 496. The following slight modification to Example #2 gives me 591:
There are no zones returned by Example #3 that are not returned with that modified Example #2.
On an OS X system running 5.3.3, Example #2 gives 407, Example #3 gives 564, and the modified Example #2 gives 565. Again, there are no zones returned by Example #3 that are not returned with that modified Example #2.
On a Linux system running 5.2.6 with the timezonedb PECL extension installed, Example #2 gives me 571 zones and Example #3 gives me only 488. There are no zones returned by Example #3 that are not by Example #2 on this system. The constant DateTimeZone::ALL_WITH_BC does not seem to exist in 5.2.6; it was probably added in 5.3.0.
So it seems the simplest way to get a list of all time zones in 5.3.x is
timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)
, and in 5.2.x istimezone_identifiers_list()
. The simplest (if not fastest) way to check if a particular string is a valid time zone is still probably@timezone_open($timezoneId) !== false
.