Работа с часовыми поясами(таймзонами) в php и mysql

Сложности работы с часовыми поясами, думаю, возникают у каждого кто сталкивается с этим впервые. Для примера рассмотрим создание планировщика: пользователь составляет расписание с заданиями — указывает время когда задание начинается и когда заканчивается.
Но, т.к. пользователи находятся в разных странах и, соответственно, в разных часовых поясах, возникает вопросы: “А как хранить время?” и “И как искать задачи, которые надо запустить/остановить сейчас?”

Что такое часовой пояс и какие возникают проблемы?

Если не принимать во внимание часовой пояс пользователя, то можно хранить время сервера, на котором запущено приложение. И по крону искать задачи которые уже наступили или уже/еще не работают. Но все меняется, если начать учитывать часовые пояса.

Часовой пояс — это величина, выраженная в часах и минутах, которое показывает смещение в одну или другую сторону относительно стандартного времени. Этим стандартным временем является UTC. Например, в Москве смещение “UTC+03:00”, а в Лос-Анжелесе “UTC-08:00”. А еще смещение может быть “летним” и “зимним”. Например, в Лос-Анжелесе летнее смещение “UTC-07:00”. И для пущей гемаройности перевод времени производится по разному — в Европе переход на летнее время осуществляется в 01:00 по UTC; в США и Канаде с 2007 года переход на летнее время осуществляется во второе воскресенье марта в 2:00, а обратный переход — в первое воскресенье ноября, также в 2:00. Т.е. перевод времени в разных странах производится в разное время. А в некоторых странах, например в России, время не изменяется.

Все вот “это” нужно учесть при создание планировщика. Не буду писать как постепенно прийти к правильным решениям -это хорошо описано в статье “Never say never» или Работаем с таймзонами правильно”. Сразу напишу, как поступить в нашем случае:

  • хранить время сразу в нескольких вариантах:
    • UTC. По utc удобно искать задачи которые нужно выполнить в текущий момент, т.к. это время одинаково для всех;
    • название часового пояса. Это нужно, для того что бы перевести местное время пользователя ко время utc(т.к. пользователь указывает в расписании именно местное время). Обычно этот параметр хранится в профиле пользователя
  • местное время. Казалось бы избыточный параметр, но очень полезный — не надо постоянно пересчитывать utc время в местное, чтобы отобразить его, например, в календаре нашего планировщика;
  • крон скрипт, который каждую минуту ищет задачи которые надо завершить или начать. Поиск по utc очень прямолинейный и простой;
  • крон скрипт, который пересчитывает utc время нашего задания после изменения времени с “зимнего” на “летнее” и обратно. Да, да — именно utc параметр, а не местное время. Местное время же не изменяется -вы как вставали(или ложились) в 7 часов утра, так и продолжаете это делать в 7 часов утра

Инструменты и библиотеки

  1. Список часовых поясов;
  2. На основе этого списка сделал mysql таблицу timezones.sql;
  3. для JS есть библиотека Moment.js. Она умеет преобразовывать даты, в том числе учитывая тайм-зоны. Причем у них есть довольно большой файл для локализации и учета часовых поясов;
  4. PHP расширение pecl, которое исправляет недостатки встроенных таймзон в php — timezonedb;
  5. определение часового пояса посетителя по его IP можно сделать с помощью api от maxmind;
  6. Google Maps Time Zone API обеспечивает простой интерфейс для запроса сведений о часовом поясе для какого-либо местоположения на Земле, а также о разнице во времени относительно UTC для этого местоположения;
  7. timezonedb.com — удобная, постоянно обновляемая база часовых поясов. Содержит не только смещение в конкретном часовом поясе в текущий момент, но и в прошлом. И вроде бы недалекое будущее;
  8. json список всех зон https://github.com/dmfilipenko/timezones.json/blob/master/timezones.json

Немного практики

  1. Определить в php летнее сейчас время или нет.
    1
    2
    3
    4
    #is_dst_time.php
    $dt = new \DateTime();
    $dt->setTimezone(new \DateTimeZone($timezone));
    $is_dst = $dt->format('I');
  2. Определение сдвига относительно UTC с помощью JS.
    В каких то случаях этого бывает достаточно. Если ваш сервис предусматривает профили пользователей, то я советую не ориентироваться на данные полученные через JS — лучше спросите у пользователя его часовой пояс.

    1
    2
    //get_user_timezone_offset.js
    var user_timezone_offset = (-new Date().getTimezoneOffset()/60)
  3. Корректная работа с временем в mysql.
    Бывает так, что на сервере установлено время не в нулевой зоне(UTC). Поэтому приходится учитывать сдвиг времени относительно UTC. Подробно написано о этой проблеме на Stack Overflow — How do I set the time zone of MySQL?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #sheduller_with_timezones.SQL
    #Сначала получим значения свига $diff
    SELECT substring_index(TIMEDIFF(UTC_TIMESTAMP, now()), ':', 2) AS diff;
       
    #Теперь получим корректное время, поставив $diff
    SELECT CONVERT_TZ(from_unixtime(`utc_start_time`), '+00:00', '{$diff}') AS START FROM `calendar_tbl`
       
    #А так можно получить задачи, которые активны сейчас
    SELECT *
    FROM `calendar`
    WHERE UTC_TIMESTAMP() BETWEEN
      (CONVERT_TZ(from_unixtime(`utc_start_time`), '+00:00', '{$diff}')) AND
      (CONVERT_TZ(from_unixtime(`utc_end_time`), '+00:00', '{$diff}'))

    p.s. что то не так? Или вы работаете с часовыми поясами по другому? Писал статью не только, что бы поделиться опытом, но чтобы понять правильно я делаю или нет.