Как проверить, действителен ли идентификатор часового пояса из кода?


Я попытаюсь объяснить, в чем здесь проблема.

Согласно списку поддерживаемых часовых поясов из руководства по PHP, я могу видеть все допустимые идентификаторы TZ в PHP.

Мой первый вопрос заключается в том, как получить этот список из кода, но это не то, что мне действительно нужно.

Моя конечная цель - написать функцию isValidTimezoneId(), которая возвращает TRUE, если часовой пояс действителен, в противном случае он должен возвращать FALSE.

function isValidTimezoneId($timezoneId) {
  # ...function body...
  return ?; # TRUE or FALSE
  }

Итак, когда я передаю идентификатор TZ с помощью $timezoneId (строка) в функции мне нужен логический результат.

Ну, что у меня есть до сих пор...

1) Решение с использованием оператора @

Первое решение, которое у меня есть, это что-то вроде этого:

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
  }

Это работает отлично, но я хочу другое решение (чтобы избежать попыток установить неправильный часовой пояс)

2) Решение с использованием списка идентификаторов часовых поясов()

Затем я пытался получить список допустимых идентификаторов часовых поясов и сверить его с параметром, используя функция in_array(). Поэтому я попытался использовать timezone_identifiers_list(), но это было не так хорошо, потому что в массиве, возвращаемом этой функцией, отсутствовало много часовых поясов (псевдоним DateTimeZone::listidentifiers()). На первый взгляд это было именно то, что я искал.

function isValidTimezoneId($timezoneId) {
  $zoneList = timezone_identifiers_list(); # list of (all) valid timezones
  return in_array($timezoneId, $zoneList); # set result
  }

Этот код выглядит красиво и просто, но я обнаружил, что массив $zoneList содержит ~400 элементов. По моим расчетам, он должен возвращать более 550 элементов. 150+ элементов являются потерянный... Так что этого недостаточно для решения моей проблемы.

3) Решение на основе Зоны времени::Список изменений()

Это последний шаг на моем пути, когда я пытаюсь найти идеальное решение. Используя массив, возвращаемый этим методом, я могу извлечь все идентификаторы часовых поясов, поддерживаемые 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);
  }

Эта функция возвращает 563 элемента (в Example #2 у меня есть только 407).

Я пытался найти различия между этими двумя массивы:

$a1 = timezone_identifiers_list();
$a2 = createTZlist();

print_r(array_values(array_diff($a2, $a1)));

Результат:

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
)

Этот список содержит все допустимые идентификаторы TZ, которым Example #2 не удалось соответствовать.

Есть еще четыре идентификатора TZ (часть $a1):

print_r(array_values(array_diff($a1, $a2)));

Вывод

Array
(
    [0] => America/Bahia_Banderas
    [1] => Antarctica/Macquarie
    [2] => Pacific/Chuuk
    [3] => Pacific/Pohnpei
)

Итак, теперь у меня есть почти идеальное решение...

function isValidTimezoneId($timezoneId) {
  $zoneList = createTZlist(); # list of all valid timezones (last 4 are not included)
  return in_array($timezoneId, $zoneList); # set result
  }

Это мое решение, и я могу его использовать. Конечно, я использую эту функцию как часть класса, поэтому мне не нужно генерировать $zoneList при каждом вызове методов.

Что мне действительно нужно здесь?

Мне интересно, есть ли более простое (быстрое) решение для получения списка всех допустимых идентификаторов часовых поясов в виде массива (я хочу избежать извлечения этого списка из DateTimeZone::listAbbreviations(), если это возможно)? Или, если вы знаете другой способ проверить , действителен ли параметр часового пояса , пожалуйста, дайте мне знать (повторяю, оператор @ не может быть частью решения).


P.S. Если вам нужны более подробные сведения и примеры, дайте мне знать. Я думаю, что ты не знаешь.

Я использую PHP 5.3.5 (думаю, что это не так важно).


Обновление

Любая часть кода, которая создает исключение для недопустимой строки часового пояса (скрытая с помощью @ или пойманная с помощью блока try..catch), не является решением, которое я ищу.


Еще одно обновление

Я назначил небольшую награду за этот вопрос!

Теперь я ищу самый простой способ извлечь список всех идентификаторов часовых поясов в массиве PHP.

Author: Wh1T3h4Ck5, 2011-04-28

8 answers

Ваше решение отлично работает, поэтому, если вы ищете скорость, я бы более внимательно посмотрел на то, что вы делаете со своими массивами. Я рассчитал несколько тысяч испытаний, чтобы получить разумное среднее время, и вот результаты:

createTZlist  : 20,713 microseconds per run
createTZlist2 : 13,848 microseconds per run

Вот более быстрая функция:

function createTZList2() {
  $out = array();
  $tza = timezone_abbreviations_list();
  foreach ($tza as $zone)
    foreach ($zone as $item)
      $out[$item['timezone_id']] = 1;
  unset($out['']);
  ksort($out);
  return array_keys($out);
}

Тест if быстрее, если вы сократите его до if ($item['timezone_id']), но вместо того, чтобы запускать его 489 раз, чтобы поймать один случай, после этого быстрее отменить установку пустого ключа.

Настройка хэш-ключей позволяет нам пропустить вызов array_unique(), который стоит дороже. Сортировка ключей и последующее их извлечение происходит немного быстрее, чем их извлечение и последующая сортировка извлеченного списка.

Если вы отбросите сортировку (которая не нужна, если вы не сравниваете список), она сократится до 12 339 микросекунд.

Но на самом деле вам все равно не нужно возвращать ключи. Глядя на целостный isValidTimezoneId(), вам было бы лучше сделать это:

function isValidTimezoneId2($tzid){
  $valid = array();
  $tza = timezone_abbreviations_list();
  foreach ($tza as $zone)
    foreach ($zone as $item)
      $valid[$item['timezone_id']] = true;
  unset($valid['']);
  return !!$valid[$tzid];
}

То есть, предполагая, что вам нужно только тестируйте один раз за выполнение, в противном случае вы захотите сохранить $valid после первого запуска. Этот подход позволяет избежать необходимости выполнять сортировку или преобразование ключей в значения, поиск ключей выполняется быстрее, чем поиск in_array(), и нет дополнительного вызова функции. Установка значений массива в true вместо 1 также удаляет одно приведение, когда результат истинен.

Это надежно снижает его до менее чем 12 мс на моей тестовой машине, почти в два раза быстрее, чем в вашем примере. Забавный эксперимент в микрооптимизации!

 15
Author: Cal, 2013-06-26 20:49:11

Почему бы не использовать оператор @? Этот код работает довольно хорошо, и вы не меняете часовой пояс по умолчанию:

function isValidTimezoneId($timezoneId) {
    @$tz=timezone_open($timezoneId);
    return $tz!==FALSE;
}

Если вы не хотите @, вы можете сделать:

function isValidTimezoneId($timezoneId) {
    try{
        new DateTimeZone($timezoneId);
    }catch(Exception $e){
        return FALSE;
    }
    return TRUE;
} 
 23
Author: hectorct, 2011-04-28 18:19:22

Когда я попробовал это в системе Linux под управлением 5.3.6, ваш пример № 2 дал мне 411 зон, а пример № 3 - 496. Следующая небольшая модификация примера № 2 дает мне 591:

$zoneList = timezone_identifiers_list(DateTimeZone::ALL_WITH_BC);

Нет зон, возвращаемых примером № 3, которые не возвращались бы с этим измененным примером № 2.

В системе OS X, работающей под управлением 5.3.3, Пример № 2 дает 407, Пример № 3 дает 564, а измененный пример № 2 дает 565. Опять же, в примере № 3 нет зон, которые не были бы возвращены с этим измененным примером № 2.

В системе Linux под управлением 5.2.6 с установленным расширением timezonedb PECL Пример № 2 дает мне 571 зону, а пример №3 - только 488. В этой системе нет зон, возвращаемых примером № 3, которые не соответствуют примеру № 2. Постоянная временная зона::ALL_WITH_BC, похоже, не существует в 5.2.6; вероятно, она была добавлена в 5.3.0.

Таким образом, кажется, что самый простой способ получить список всех часовых поясов в 5.3.x - это timezone_identifiers_list(DateTimeZone::ALL_WITH_BC), и в 5.2.x это timezone_identifiers_list(). Самый простой (если не самый быстрый) способ проверить, является ли конкретная строка допустимым часовым поясом, все еще, вероятно, @timezone_open($timezoneId) !== false.

 5
Author: Anomie, 2011-05-03 17:05:36

Если вы работаете в Linux, большая часть, если не вся информация о часовых поясах хранится в /usr/share/zoneinfo/. Вы можете пройти по ним, используя is_file() и связанные с ними функции.

Вы также можете проанализировать предыдущие файлы с помощью zdump для кодов или извлечь источники для этих файлов и удалить/вырезать необходимую информацию. Опять же, вы не обязаны использовать встроенные функции для выполнения этой задачи. Нет никакого обоснования, почему кто-то может заставить вас использовать только встроенные функции даты.

 1
Author: sanmai, 2011-05-09 05:46:53
  1. Смотрите в источниках PHP на php_date.c и карта часового пояса.h вот почему я могу сказать, что это всегда в 101,11111% статической информации (но для сборки php).

  2. Если вы хотите получить его динамически, используйте timezone_abbreviations_list как DateTimeZone::listabbreviations - это сопоставление с ним.

  3. Как вы можете видеть, все эти значения являются всего лишь однократно заполненным списком для текущей версии PHP.

Настолько простое и быстрое решение - подготовить каким-то образом статический файл с полученными идентификаторами один раз на сервер во время установки вашего приложения и используйте его.

Например:

function isValidTZ($zone) {
    static $zones = null;
    if (null === $zones) {
        include $YOUR_APP_STORAGE . '/tz_list.php';
    }
    // isset is muuuch faster than array_key_exists and also than in_array
    // so you should work with structure like [key => 1]
    return isset($zones[$zone]);
}

Tz_list.php должно быть так:

<?php
$zones = array(
    'Africa/Abidjan' => 1,
    'Africa/Accra' => 1,
    'Africa/Addis_Ababa' => 1,
    // ...
);
 1
Author: gaRex, 2011-05-09 11:58:38

Я бы исследовал, что изменяет идеальный массив, и использовал базовый механизм кэширования (например, сохранял массив в файле, который вы включаете и обновляете при необходимости). В настоящее время вы оптимизируете построение массива, который является статичным для 99,9999 % всех запросов.

Редактировать: Хорошо, статический/динамический.

if( !function_exists(timezone_version_get) )
{
    function timezone_version_get() { return '2009.6'; }
}
include 'tz_list_' . PHP_VERSION . '_' . timezone_version_get() . '.php';

Теперь при каждом обновлении версии php файл должен автоматически обновляться вашим кодом.

 1
Author: Mel, 2011-05-09 21:33:37

В случае php

public static function is_valid_timezone($timezone)
{
    $now_timezone = @date_default_timezone_get();

    $result = @date_default_timezone_set($timezone);

    if( $now_timezone ){    
        // set back to current timezone
        date_default_timezone_set($now_timezone);
    }

    return $result;
}
 1
Author: user2595120, 2013-11-27 09:11:27

Просто дополнение к превосходному ответу Кэла. Я думаю, что следующее может быть еще быстрее...

function isValidTimezoneID($tzid) {
    if (empty($tzid)) {
        return false;
    }
    foreach (timezone_abbreviations_list() as $zone) {
        foreach ($zone as $item) {
            if ($item["timezone_id"] == $tzid) {
                return true;
            }
        }
    }
    return false;
}
 0
Author: Will Voelcker, 2016-05-14 20:14:25