Компания
917,35
рейтинг
10 ноября 2014 в 22:22

Разработка → «Never say never» или Работаем с таймзонами правильно

Эта статья рассказывает о проблемах, которые поджидают программиста, работающего с часовыми поясами. В теории, вроде, всё хорошо, просто и понятно, но жизнь — штука сложная, и на практике, порой, возникают совершенно неожиданные ситуации.

TL;DR: Работа с таймзонами — это боль и унижение. Никогда не работайте с таймзонами!

Итак, все кругом твердят вам, что при получении времени от пользователя нужно сразу же переводить его в UTC, работать со временем нужно только в UTC и хранить время тоже нужно строго в UTC. Совет, на первый взгляд, выглядит разумным, и следование ему делает вашу жизнь проще… Если только ваша программа не предполагает сложной работы с датами. Записать в базу данных дату и время регистрации пользователя на сайте? Сохранить время отправки сообщения или дату создания заказа в интернет-магазине? Вывести сообщение в лог с указанием даты-времени? Используйте UTC и всё будет в порядке, можете даже не читать эту статью дальше. Любое текущее время можно совершенно спокойно конвертировать в UTC и забыть о проблемах. Но что, если мы хотим работать с временем в будущем? Или в прошлом? Например, если мы пишем сервис календаря, или сервис для отложенной отправки сообщений?

UTC не панацея

Поясню на примере. Допустим, мы создали тот же сервис отложенных сообщений. Зайдя на наш сайт пользователь может создать себе напоминание на любое время (разумеется, в будущем) по почте или СМС. Сайт наш предельно прост: задаём дату, время, вводим текст напоминания и канал связи (адрес email или номер телефона), полученные от пользователя данные складываем в базу и потом периодически делаем по ней выборки и отправляем сообщения. Всё, профит и уважение благодарных людей!

Нет, не всё. Следуя совету всегда везде хранить всё в UTC, мы преобразовали полученную от пользователя дату и время в UTC и положили их в базу данных. Пусть пользователь из Москвы зашёл на наш сайт 2 марта 2014 года и создал напоминание на 09:00 утра 3 ноября 2014 года. Соответственно, в базу мы положили значение «2014-11-03 05:00:00», ведь в тот день, 2 марта 2014 года, смещение для таймзоны «Europe/Moscow» для 3 ноября 2014 года составляло «UTC+4».

Понимаете, к чему я клоню?

Да, 21 июля 2014 года Государственная дума Российской Федерации приняла законопроект об отмене летнего времени. Согласно этому закону, с 26 октября 2014 года, смещение для таймзоны Europe/Moscow стало «UTC+3» вместо «UTC+4» (а ещё переход на летнее время отменили, но речь сейчас не об этом). Соответственно, если мы отправим уведомление пользователю 3 ноября в 5:00 утра по UTC, он получит его в 8:00 утра по Москве, и я уверен, что пользователь будет недоумевать, ведь он просил, чтобы уведомление пришло ему ровно в девять утра.

Вывод прост: вы можете хранить время в UTC, но только для событий в настоящем и недавнем прошлом, то есть для тех дат, таймзона которых заведомо не изменится. Хранить время в UTC для дат в будущем опасно, ведь никто не знает, какие ещё законы примут правительства каких стран, и что станет с таймзонами через десять лет, пять лет, или даже через год.

С другой стороны, если вы будете хранить в базе данных локальное время пользователя и его таймзону, работать с такими данными будет практически невозможно. Вернёмся к нашему примеру сервиса уведомлений: два пользователя создали по уведомлению. Первый пользователь из Москвы, попросил прислать ему СМС 15 декабря 2014 года в 15:00 (пишем в базу его локальное время «2014-12-15 15:00:00» и его часовой пояс «Europe/Moscow»). Второй пользователь из Нью-Йорка, попросил прислать ему письмо на электронную почту 15 декабря 2015 года в 7:00PM (пишем в базу его локальное время «2014-12-15 19:00:00» и его часовой пояс «America/New_York»). Пока всё хорошо: у нас записано локальное время, в которое пользователь хотел бы получить своё уведомление, и он его получит строго в это время, даже если правительство одной из этих стран изменит один из этих часовых поясов (смещение, правила перехода на летнее время, всё, что угодно).

Проблемы начинаются, когда вы будете писать скрипт, выбирающий из базы уведомления для отправки. Если бы все даты были записаны в UTC, всё было бы просто, — каждую минуту выбираем сообщения для отправки:
SELECT * FROM reminders WHERE remind_time < NOW();

При условии, что «SELECT NOW();» возвращает время в UTC. Но мы записали в базу локальное время пользователя и его часовой пояс, что же делать? Страдать :-) Ведь «NOW()» по UTC — это "+3" часа в Москве (и сообщение уже опоздало) и "-5" часов в Нью-Йорке (сообщение ещё рано отправлять).

Нет, конечно можно придумать много способов выборки из базы тех уведомлений, которые пора отправлять, но все они на более-менее нагруженном сервисе приведут к проблемам с производительностью, да и вообще мы же хотим сделать всё правильно, без «костылей», да?

Какие есть варианты? Их много, однако я вижу только один более-менее приемлимый вариант: хранить в базе три значения: время в UTC (для выборки по этому полю), локальное время пользователя и его часовой пояс (таймзону). Да, у нас будут храниться избыточные данные, однако я не знаю ни одного нагруженного сервиса, который не прибегал бы к денормализации данных. В реальном мире это нормально. Какие плюсы мы получаем? В случае изменений часовых поясов, мы можем пройтись по записям для изменившихся таймзон специальным скриптом, и обновить время в UTC, если оно поменялось в результате обновления часового пояса. По моему скромному мнению, это хороший компромисс.

Всё ещё хуже, чем кажется

Вроде всё, да? Нет, мы только начали :-) Правительство может не только менять конфигурацию часовых поясов, но и добавлять новые и выкидывать старые таймзоны. Так, например, для жителей Российского города Чита (и не только для него, но сейчас не об этом) с 26 октября 2014 года был введён новый часовой пояс «Asia/Chita» (раньше такого часового пояса не существовало) вместо употреблявшегося до этого «Asia/Yakutsk». Разница с UTC у прежнего часового пояса («Asia/Yakutsk») составляет "+09:00", а у нового часового пояса («Asia/Chita») эта разница составляет "+08:00". Проблема заключается в том, что мы храним в базе только время и часовой пояс пользователя, но не его географическое положение. И для записей с часовым поясом «Asia/Yakutsk» мы никак не можем знать, из Читы ли наш пользователь, или из Якутстка, и мы никак не можем достоверно определить время отправки сообщения пользователю. Шах и мат! Не забываем страдать, друзья.

Если у вас есть возможность узнать географическое положение пользователя и при следующем его заходе на сайт определить, что он находится в регионе со сменившейся таймзоной (Чита для случая выше), можно спросить у него правильный часовой пояс. И предложить обновить таймзону для всех его событий (с пересчётом времени в UTC для каждого события), но здесь тоже могут возникнуть подводные камни и нюансы, выходящие за рамки данной статьи. Кстати, отчасти по этой причине мы в настройках Календаря mail.ru просим пользователя выбрать его географическое местоположение (город), а не часовой пояс, как это делают остальные сервисы :-) И даже несмотря на это, скажу честно, периодически бывают проблемы.

С хранением времени в прошлом тоже не всё так просто. Если это прошлое — относительно недавнее (скажем, речь идёт о двадцать первом веке), то проблем с хранением времени в UTC быть не должно (хотя гарантий вам, конечно, никто не даст). Если же речь идёт о двадцатом веке или (о, ужас) более давних временах, проблемы гарантированы. Начнём с того, что для многих периодов истории прошлого века, информация о переводе часов постоянно меняется по сей день. Так, например, в обновлении базы данных часовых поясов tzdata версии 2014g от 30 августа 2014 года для ряда часовых поясов СССР были внесены изменения на несколько секунд или минут для дат до 1926 года. Просто кто-то заметил несоответствие и уведомил об этом составителей tzdata. Или вот ещё пример из более близких нам времён: в обновлении tzdata версии 2014a от 9 марта 2014 года изменилась информация о дате перехода Украины с Московского времени на Восточноевропейское: этот переход произошёл не первого января 1992 года (как было записано в этой базе), а первого июля 1990 года.

База данных часовых поясов обновляется несколько раз в год, во всём мире появляются новые таймзоны, меняются правила существующих, актуализируется информация о прошлом времени, постоянно происходят какие-то изменения, и их постоянно необходимо учитывать.

Как же всё-таки правильно хранить время?

Итак, как же всё-таки правильно хранить время в базе данных? Лучше, конечно, этого не делать, однако если очень нужно, то вот мои личные рекомендации (буду рад услышать критику или предложения):
  1. Если вам нужно хранить время только что произошедшего события, текущее время, по факту определённого действия, храните его в UTC. Это могут быть записи в логах, время регистрации пользователя, совершения заказа или отправки письма.
  2. Если время не привязано к пользователю или его часовому поясу, храните его в UTC. Это может быть, например, время следующего солнечного затмения.
  3. Если вам нужно хранить время в прошлом или в будущем, сохраняйте локальное время пользователя, а рядом сохраняйте его таймзону. А ещё лучше, так, чтобы наверняка, сохраняйте географическое положение пользователя. Если нужно делать выборки по этому времени, сохраняйте рядом время в UTC, и обновляйте это время при изменении информации о часовом поясе.
  4. Если вам нужно совершенно точно знать время для любой даты для заданного географического положения (например, для астрономических расчётов) — храните точные координаты пользователя, но не его часовой пояс. Впрочем, если перед вами стоит такая задача, то вы и так знаете, как делать правильно.

Первый вариант покрывает возможные сценарии использования для 99% программ и, вполне возможно, вам этого будет достаточно. Однако необходимо чётко понимать и осознавать выбор того или иного варианта дейстий.

Работаем со временем

С хранением времени, вроде, разобрались. Однако часто можно услышать так же совет «всегда работайте с временем в UTC». Подразумевается, что как только вы получили время от пользователя, его нужно сразу же перевести в UTC и работать только с временем в UTC. Звучит логично, не правда ли?

Неправда. По крайней мере, не во всех случаях, и вот вам конкретный пример.

Вернёмся к нашему примеру с сервисом отложенных сообщений. Всё хорошо, сервис развивается, пользователи довольны, но просят добавить функционал повторяющихся уведомлений. А повторы бывают не только простые («каждый день», «через день», «каждый месяц»), но и достаточно сложные («каждую неделю по вторникам», «каждый месяц в последнюю пятницу месяца» и т.д.). Чтобы не писать свой велосипед для этих повторов, изучим уже готовые решения. Существует такое понятие, как «повторяющиеся события». Существует специальный формат описания правил повторения, который, конечно, учитывает не все возможные варианты (например, нельзя задать «два дня через два»), однако большую часть случаев он покрывает. Примеры применения этого формата можно увидеть в описании поля RRULE спецификации iCalendar и в документации объекта rrule модуля python-dateutil для Python.

Возьмём модуль python-dateutil и используем его в нашем коде. Вроде всё должно быть хорошо, однако пользователи жалуются, и исследование этих жалоб приводит нас с достаточно неожиданным результатам.

Один из вариантов повторяющихся событий — повтор по дням недели. Мы можем описать событие, которое повторяется, например, в 12:00 каждую неделю по вторникам и пятницам. Вот как это может выглядеть на практике, в реальном коде:
>>> import datetime
>>> from dateutil import rrule
>>> list(rrule.rrule(rrule.WEEKLY, count=4, byweekday=(rrule.TU, rrule.FR),
                     dtstart=datetime.datetime(2014, 11, 3, 12, 0)))
[datetime.datetime(2014, 11, 4, 12, 0),
 datetime.datetime(2014, 11, 7, 12, 0),
 datetime.datetime(2014, 11, 11, 12, 0),
 datetime.datetime(2014, 11, 14, 12, 0)]

Казалось бы, всё хорошо. Теперь давайте представим себе, что пользователь из Москвы создал повторяющееся событие, которое происходит в час ночи. Как только мы получили от него время «2014-11-03 01:00:00» мы, согласно рекомендациям умных людей, сразу же переводим его в UTC (процесс перевода нас сейчас не интересует, нам следует знать, что фактически мы отнимаем три часа от полученного времени), и получаем следующее время в UTC: datetime.datetime(2014, 11, 2, 23, 0). Пока всё хорошо. Давайте получим повторы для полученного времени:
>>> list(rrule.rrule(rrule.WEEKLY, count=4, byweekday=(rrule.TU, rrule.FR),
                     dtstart=datetime.datetime(2014, 11, 2, 23, 0)))
[datetime.datetime(2014, 11, 4, 23, 0),
 datetime.datetime(2014, 11, 7, 23, 0),
 datetime.datetime(2014, 11, 11, 23, 0),
 datetime.datetime(2014, 11, 14, 23, 0)]

Кажется, что-то пошло не так. Если мы переведём полученные значения в локальное время пользователя (прибавим к каждому три часа), мы увидим, что повторы сдвинулись и событие повторяется всё так же в час ночи, но уже по средам и субботам. И это не ошибка модуля python-dateutil, код отработал корректно. Это наша ошибка, в этом конкретном случае нам нужно было работать с локальным временем пользователя.

Кстати, многие сервисы календарей имеют этот баг, например, программа iCal в OS X, в определённых случаях, считает повторы совершенно неправильно.

Не забывайте страдать

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

А работа с таймзонами — это боль и страдание, да. Если есть хоть малейшая возможность не работать с ними — воспользуйтесь ей, не пожалеете. Напоследок приведу пару примеров некорректной работы реальных программ:

Python 2.7.6
➜ date
воскресенье,  9 ноября 2014 г. 22:44:32 (MSK)
➜ python -c "import datetime; print datetime.datetime.now()"
2014-11-09 22:44:33.310904
➜ python -c "import datetime; print datetime.datetime.utcnow()"
2014-11-09 19:44:34.405287

Вроде всё хорошо. Смотрим дальше:
➜ date +%z
+0300
➜ python -c "import time; print time.timezone/3600"
-4

WAT? Нет, вроде это не баг, а фича, однако никому от этого не легче. Какой вообще смысл в коде, который в любой момент может сломаться (и ломается!)?

Firefox 33.0.3
new Date(2015, 0, 6) 
"Tue Jan 06 2015 00:00:00 GMT+0300 (Russia TZ 2 Standard Time)" 

new Date(2015, 0, 7) 
"Tue Jan 06 2015 23:00:00 GMT+0300 (Russia TZ 2 Standard Time)" 

new Date(2015, 0, 8) 
"Thu Jan 08 2015 00:00:00 GMT+0400 (Russia TZ 2 Daylight Time)"

WAT? Нет, я понимаю, что этот вопрос уже много раз поднимался, но жить от этого не легче.

В общем, что могу сказать, не забывайте страдать :-)

А как вы работаете с датами, временем и таймзонами?

Владимир Рудных,
Технический руководитель Календаря Mail.Ru.
Автор: @Dreadatour

Комментарии (102)

  • –14
    Может быть мне кажется все слишком просто, но почему не хранить в базе все в unixtime? а отображать в интерфейсе и записывать в базу с учетом временной зоны пользователя(только предупреждать его об этом, если он сам не выбрал временную зону). Да, придется так же страдать из-за рандомных законопроектов, но от них никуда не деться, как ни храни.
    • +15
      Unix time чем-то отличается от UTC?
      Я к тому, что с моей колокольни, Unix time — это способ записать время в UTC одним числом, не более.
      • +1
        Так и есть. UNIX timestamp — то же самое, что UTC: ru.wikipedia.org/wiki/UNIX-время
        Время UNIX согласуется с UTC — в частности, при объявлении високосных секунд UTC соответствующие номера секунд повторяются, то есть високосные секунды не учитываются.
      • –12
        Формой записи, если в базе есть запись «2014-12-15 15:00:00», это еще не значит что она в UTC, вариантов ошибиться всегда множество.
        • +9
          Если вы не знаете, в какой таймзоне хранится время у вас в базе, то у меня для вас плохие новости :)
        • +4
          Ну знаете, если в базе есть запись «1415654842» — это тоже ничего не значит. Не факт даже, что это время.
          • +1
            Или вот ещё замечательный пример про timestamp:
            >>> import datetime
            >>> datetime.datetime.fromtimestamp(0)
            datetime.datetime(1970, 1, 1, 3, 0)
            
    • –1
      Ну вот у нас всё время в unixtime.
      И как теперь быть с заявкой пользователя из Читы, который открыл её 1 октября, а решили её 30 октября? Как вывести время открытия и решения заявки?
      Не говоря о такой мякотке, как построение отчетов в business objects, который не умеет в unixtime. И если ты хочешь в нем сделать выборку по датам, то придется unixtime из каждой строки оракловой базы переводить в Date и сравнивать со значением Date, которое пришло из BO. Миллион записей в таблице? Ну значит миллион преобразований.
      • +4
        Прошу меня извинить, но мне кажется, что вы невнимательно прочитали статью.
        И как теперь быть с заявкой пользователя из Читы, который открыл её 1 октября, а решили её 30 октября?

        Тут просто отвечу цитатой из своего текста:
        Если вам нужно хранить время только что произошедшего события, текущее время, по факту определённого действия, храните его в UTC. Это могут быть записи в логах, время регистрации пользователя, совершения заказа или отправки письма.

        Кроме того, даже если вы будете хранить локальное время пользователя, никаких проблем не возникнет. Вообще никаких. При условии, что вы пользуетесь правильными инструментами. Поясню чуть подробнее, что я имею в виду. Есть два варианта работы с таймзоной пользователя: правильный и неправильный.

        Неправильный — это когда мы узнали, что «с 26 октября 2014 года смещение для Москвы стало UTC+3» и для всех дат в таймзоне Europe/Moscow применяем это смещение. В прошлом, в будущем, — неважно. Это категорически неправильно, однако многие продукты, и достаточно серьёзные продукты, так делают, максимум, что они могут учитывать — DST.

        Правильный — это когда у нас есть база данных изменения часовых поясов (tzdata) и мы используем её. Так, согласно этой базе, до 26 октября 2014 года смещение относительно UTC было 4 часа, плюс действовал DST, а после 26 октября — три часа, без DST. И таких вот записей в базе может быть много. Но мы можем практически для любой даты получить смещение и преобразовать UTC в локальное время. И наоборот.

        Не говоря о такой мякотке, как построение отчетов в business objects, который не умеет в unixtime.

        Простите, но вот это «не умеет» — это смешно. Если я пользователям календаря скажу «вы знаете, мы пользуемся инструментом, который не умеет работать с часовыми поясами» — они плюнут на нас и уйдут к конкурентам.

        Или вот ещё глупый пример: если при запуске спутника в космос ракета на старте упадёт, и разработчик ракеты скажет «вы знаете, у нас такая мякотка, наш софт не умеет работать с часовыми поясами и поэтому в итоге всё рассчиталось неправильно», мне кажется, его никто не поймёт. А может, даже накажут.

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

        Ну и не забывайте страдать, конечно :-)
        • +2
          Эм… маленькая деталь. Вы — разработчик бесплатного сервиса. Пользователь пришел, пользователь ушел. Надо бороться за качества продукта, менять его, добавлять новые фичи.
          Я — админ купленной для большой компании ИС. И я не могу, когда захочу, потратить N часов и сделать вменяемую поддержку tzdata, например. потому что я не разаботчик этой системы, а настройками такого не добиться.

          Простите, но вот это «не умеет» — это смешно. Если я пользователям календаря скажу «вы знаете, мы пользуемся инструментом, который не умеет работать с часовыми поясами» — они плюнут на нас и уйдут к конкурентам.

          Как потенциальный пользователь календаря мэйл.ру я очень рад слышать такую позицию. Как фактический пользователь BO если я скажу «эй, чуваки, у вас тут unixtime`ом и поддержкой наших классных часовых поясов не пахнет в той древней версии, что у нас есть» — угадайте, куда я пойду?

          Ну и не забывайте страдать, конечно :-)

          Да пока пользователи не наседают, я и не страдаю. А среди них пока только 1 заметил :)
          • 0
            Вот такой подход мне понятен :-) Я, кстати, в теме всей этой внутренней кухни разработки ИС для больших компаний (так получилось), и прекрасно понимаю, о чём речь. Всему есть своя цена и всегда есть место компромиссам и понятиям целесообразности.
  • –6
    бессмысленный комментарий, простите)
    не забывайте страдать :-)
    Вперед! За благодать святую —
    Все как один. Настал черед.
    Ударим дружно, памятуя
    О смерти господа. Вперед!
    Господь наш схвачен был врагами.
    Он злые муки претерпел
    И принял смерть… Победа с нами!
    Вперед, кто доблестен и смел!
  • +5
    «Не забывайте страдать» записал себе в афоризмы.
  • +4
    Хранить гео-координаты — очень умно. Лишь бы какому-нибудь злому гению не пришло в голову «поправить» начало координат…
    • +10
      Тсс, в госдуме ещё не слышали про широту и долготу. Предлагаю им об этом и не говорить.
      • +6
        Не хочу вас расстраивать, но помимо WGS 84, с которым работает GPS и большинство веб-сервисов картографии, существуют и другие. Например Пулково-1942 (применялся в СССР и России), а сейчас ПЗ-90 — угадайте кто ее ввел. Кроме того, у многих стран тоже есть своя система координат, «наиболее лучшим образом» описывающая ее территорию. Хотя казалось бы, эллипсоид он и есть эллипсоид…
        • +3
          Не хочу показаться занудой, но, если быть чуть более точным, то геоид, а не эллипсоид.
        • +1
          В том то и дело, что геоид. А эллипсоид это так, аппроксимация.
          WGS 84 даёт самую точную аппроксимацию по миру в целом. А вот в отдельных странах может быть вариант и получше… ПЗ-90, например
          • 0
            Точно! Не так прочитал комментарий — не там расставил акценты :)
      • 0
        Они вот тут решили, что 31 декабря называть нерабочим днем — это «бесперспективно», что, на фоне иных принимаемых ими законов, и правда кажется неделовым и неполезным законом.

        Так что — тише, а то вдруг кто-то из их помощников погуглит слова «депутат» и «госдума», чтобы опнять настроения в Сети, и наткнется на ваш ответ… )
      • +1
        Мечтаю дожить до того дня, когда к власти придут программисты.
        • 0
          Система кармы и рейтинга в масштабах страны или целого мира? Не дай Бог дожить до такого.
        • 0
          Мечтаю не дожить.
          «Если бы строители строили здания так же, как программисты пишут программы, первый залетевший дятел уничтожил бы цивилизацию»
          • 0
            Тогда у каждого дома было бы по несколько цивилизаций.
            • 0
              И все 11 ⅞ неразумные :)
    • 0
      Да это ладно. Вот если начнут города двигать…
  • +4
    Ну или забыть наконец про mysql и юзать postgresql, где работать с датами можно нативно, включая таймзоны. Есть нативная функция timezone('GMT+3', ts). В добавок: не нужно хранить таймзону в отдельном поле — есть спец тип: timestamp with timezone для этого. И конвертирование вдругие таймзоны еще проще. Единственное что нужно всегда помнить: функция timezone по-разному работает для типов ts with tz и для ts without tz, тут можно немного накосячить если не понимать разницу.
    Проблемы появляются только когда происходит незапланированный во всем мире переход в другую зону как в этом году с Россией. Тут начинаются косяки с зонами типа «Europe/Moscow» т.к. сдвиг для них не обновился. Тут уж ничего не поделаешь — нужно ручками в БД править. Если не ошибаюсь, то от рута в postgresql можно исправить сдвиг прямо во встроенной в postgresql таблице таймзон (не помню уже как она называется, но она точно есть в виде обычной таблицы бд + расположена она не в схеме public)
    • +2
      Мы у себя как раз используем postgresql. Как раз в том-то и заключается проблема, что таймзоны в постгресе — ни разу не таймзоны, а тупо смещение относительно UTC, и толку от них — ну очень мало. Все описанные в статье проблемы остаются как есть :)
      • +4
        Опа опа, я ошибся: www.postgresql.org/docs/9.3/static/datatype-datetime.html#DATATYPE-TIMEZONES
        Посыпаю голову пеплом.
        • +3
          Я пока не заметил в postgresql косяков с таймзонами, так что можно вполне уверенно их использовать. Нативное почти всегда лучше. Тем более в постгресе куча плюшек для работы со временем =)
          • +1
            Согласен, postgresql вообще просто сказка после mysql :-)
            • +4
              Ура! +1 к армии перешедших на постгрес! =) Я после постргеса мускл использую только для небольших сайтов, где база по сути нужна только для хранения небольшого количества инфы, не больше. + страхую эту систему 80-100% кешированием в память ибо ну его тот мускл с его приколами к чертям =). Иногда даже использую sqlite + cache вместо mysql, если совсем уж сайтик прост
        • +3
          К сожалению, PostgreSQL не хранит информацию о часовых поясах. Когда вы делаете INSERT '2014-11-10 12:13:14 Europe/Moscow' INTO table; Постгрес конвертирует эту дату в UTC согласно часовому поясу (-3 часа для данной даты, сентябрьскую сдвинет на -4), а потом информацию о часовом поясе просто выкидывает. Увы. Надо хранить в колонке рядом всё же.

          Проверяется это легко:
          CREATE TABLE tz (
            "timestamp" timestamp with time zone
           )
          
          INSERT INTO tz ("timestamp") VALUES 
          ('2014-10-10 12:13:14 Europe/Moscow'), 
          ('2014-11-10 12:13:14 Europe/Moscow'), 
          ('2014-10-10 12:13:14 Asia/Yakutsk'), 
          ('2014-11-10 12:13:14 Asia/Yakutsk'), 
          ('2014-10-10 12:13:14 Asia/Tokyo'), 
          ('2014-11-10 12:13:14 Asia/Tokyo');
          
          SELECT * FROM tz;
                 timestamp
          ------------------------
           2014-10-10 12:13:14+04
           2014-11-10 12:13:14+03
           2014-10-10 06:13:14+04
           2014-11-10 06:13:14+03
           2014-10-10 07:13:14+04
           2014-11-10 06:13:14+03
          
          test=# SELECT "timestamp" AT TIME ZONE 'UTC' FROM tz;
                timezone
          ---------------------
           2014-10-10 08:13:14
           2014-11-10 09:13:14
           2014-10-10 02:13:14
           2014-11-10 03:13:14
           2014-10-10 03:13:14
           2014-11-10 03:13:14
          
          test=# SELECT "timestamp" AT TIME ZONE 'Asia/Yakutsk' FROM tz;
                timezone
          ---------------------
           2014-10-10 18:13:14
           2014-11-10 18:13:14
           2014-10-10 12:13:14
           2014-11-10 12:13:14
           2014-10-10 13:13:14
           2014-11-10 12:13:14
          
          test=# SELECT "timestamp" AT TIME ZONE 'Asia/Tokyo' FROM tz;
                timezone
          ---------------------
           2014-10-10 17:13:14
           2014-11-10 18:13:14
           2014-10-10 11:13:14
           2014-11-10 12:13:14
           2014-10-10 12:13:14
           2014-11-10 12:13:14
          
          • +1
            Да, всё верно. Я ошибся дважды. Посыпаю посыпанную пеплом голову пеплом ещё раз :-)

            Спасибо за замечание и исправление!

            Так получилось, что сначала подробно ответил на похожий вопрос в комментарии ниже, а потом увидел этот.
        • +2
          Хм, а что там написано, опровергающее Ваши слова?) Да, Постгрес понимает названия и аббревиатуры таймзон. Но тип timestamp with time zone всё равно хранит только UTC-время + смещение, а не название таймзоны. Хотя его, конечно, можно рядом положить, как строку…

          Я, правда, сам на своём сайте использую именно постгресовый timestamptz, но у меня нет дат в будущем, так что пока более-менее всё нормально.

          Спасибо, кстати, за статью. Хранить город (координаты) пользователя — это классная идея. Но поделитесь, как вы потом переводите эту точку в таймзону? Где-то есть актуальные границы таймзон? Я знаю только efele.net/maps/tz/, но там последнее обновление было год назад…
          • +1
            Да, всё правильно, я ошибся дважды. Огромное спасибо за замечание и подробный комментарий!

            Сначала я написал то, что знал и то, что мы у себя в проекте используем (точнее не используем таймзоны в постгресе), а потом, на всякий случай, полез в документацию и удивился, увидев там «PostgreSQL allows you to specify… а full time zone name, for example America/New_York». Подумал, что я осёл и на тот момент, когда я разрабатывал структуру базы данных я просто упустил этот момент из виду и мне стало стыдно.

            Теперь я вспомнил, почему я этого не помню и почему мы у себя в календаре не используем «timestamp with timezone» — потому, что таймзоны в постгресе просто никакие и я вообще забыл про их существование, сосредоточившись за эти годы на поддержку таймзон в коде.

            Всё равно нет мне оправдания. Буду страдать :-)

            У нас в mail.ru есть специальная геобаза, поддерживаемая в актуальном состоянии. Мы используем её для определения города пользователя по его IP (в базе мы храним город) и для определения таймзоны по городу (и, соответственно, по IP). С городами, кстати, тоже есть проблемы — были случаи, когда города переходили от одной страны к другой, и тогда нам по цепочке нужно было менять таймзону для пользователя, город которого не поменялся. Всякое бывает.

            Конечно, наша геобаза используется только внутри компании и я не могу ничего про неё говорить, однако недавно я отвечал на похожий вопрос, и нашёл модуль pythonhosted.org/python-geoip/, который позволяет определять таймзону по IP:
            from geoip import geolite2
            
            match = geolite2.lookup('17.0.0.1')
            match.timezone
            'America/Los_Angeles'
            

            Они используют геобазу от MaxMind. Я не знаю, насколько она актуальна, но беглое изучение документации показало, что там есть координаты и таймзона, и я думаю, что из этого модуля можно получить интересующую информацию.

            Ещё раз спасибо за комментарий :-)
            • 0
              Спасибо за ссылку! Они используют вот эту базу от MaxMind: dev.maxmind.com/geoip/geoip2/geolite2/.

              Но она, к сожалению, тоже не актуальна — отсутствуют новые зоны Asia/Chita и Asia/Srednekolymsk, а значит, недавняя перетасовка поясов в России там не учтена…
            • 0
              Нет, таймзоны в Постгресе хорошие, просто он их не хранит :-(

              Вот, например, как можно сделать поиск записей, которые нужно «подвинуть» после изменения часовых поясов (если, конечно, в Постгресе актуальная база часовых поясов):

              SET TIME ZONE UTC;
              
              CREATE TABLE tztest (
                "utc"      timestamp with time zone,
                "local"    timestamp with time zone,
                "timezone" character varying
              );
              
              -- Вставляем дату и время до перевода часов
              INSERT INTO tztest (utc, local, timezone) VALUES
              ('2014-11-10 12:13:14', '2014-11-10 16:13:14', 'Europe/Moscow');
              
              -- Вставляем дату и время после перевода часов
              INSERT INTO tztest (utc, local, timezone) VALUES
              ('2014-11-10 13:13:14', '2014-11-10 16:13:14', 'Europe/Moscow');
              
              
              SELECT utc, local, timezone, utc2local, utc2local = local AS ok FROM (
                SELECT utc, local, timezone, (utc AT TIME ZONE timezone) AS utc2local FROM tztest
              ) tzt;
              
                        utc           |         local          |   timezone    |      utc2local      | ok
              ------------------------+------------------------+---------------+---------------------+----
               2014-11-10 12:13:14+00 | 2014-11-10 16:13:14+00 | Europe/Moscow | 2014-11-10 15:13:14 | f
               2014-11-10 13:13:14+00 | 2014-11-10 16:13:14+00 | Europe/Moscow | 2014-11-10 16:13:14 | t
              (2 rows)
              
              DROP TABLE tztest;
              
              • 0
                Спасибо, полезный запрос! :-)
            • 0
              А может эту «специальную геобазу» того… открыть в виде api? Или хотя бы выложить в виде обновляемых раз в сутки csv-файлов для скачивания?
  • +2
    >В случае изменений часовых поясов, мы можем пройтись по записям для изменившихся таймзон специальным скриптом, и обновить время в UTC, если оно поменялось в результате обновления часового пояса.

    Перечитал несколько раз. Может я туплю, простите тогда, но мы тут точно должны обновить время в UTC? А разве не локальное время юзера?

    P.S. За статью спасибо, понравилось. Дьявол как всегда…
    • +1
      Я рассматриваю случай, когда пользователь изначально задал время в своём часовом поясе. Время в UTC нужно только для того, чтобы облегчить выборку по базе. В случае изменения конфигурации часового пояса, локальное время пользователя останется неизменным (он хотел получить уведомление в 10:00 по Москве, например), но время в UTC для этого локального времени изменится. Поэтому нам нужно обновить его.
      • 0
        Понял. Но ведь двигать значение времи в поле по которому делается выборка тоже небезопасно — можно попасть в ту же ловушку, когда «время еще не наступило», обновляем поле, получаем «время уже прошло»? Что-то тут далеко не просто получается, особенно, если событие попадает в момент перевода часов =)

        Интересно, правда ли, что тот же РЖД «внутри» весь живет по одному времени (msk)? В принципе их можно понять.
        • 0
          Вообще, по косвенным признакам РЖД действительно по Москве живет.
          Ну, по всей стране и на билетах время московское, по крайней мере.
        • 0
          Эм, что значит «внутри»? Вы на вокзале давно были? Открою вам страшную тайну, но по всей России матушке все вокзальные часы показывают одинаковое московское время и никто этого не скрывает.
          • 0
            Эк вас помотало, по всей стране проехать, по всем вокзалам — часы сверять. Да вам премию надо выдать!
        • 0
          Совершенно верно, везде есть нюансы, и, например, мы не можем совершенно однозначно и точно перевести время 1:30 ночи в Москве 26 октября 2014 года в UTC. Боль и унижение :-) Однако с этим можно что-то придумать, всё зависит от ситуации. Как я писал в статье:
          никогда не слушайте категоричных утверждений, рекомендующих вам никогда не делать тех или иных вещей

          :-)
  • 0
    Соответственно, если мы отправим уведомление пользователю 3 ноября в 5:00 утра по UTC, он получит его в 8:00 утра по Москве

    Почему? Это произойдёт только если на сервере время не будет переведено. А если будет — то он получит уведомление как раз в 9 утра. А проблема перевода времени на серверах — это не проблема хранения даты в БД.
    • 0
      Это произойдёт только если на сервере время не будет переведено. А если будет — то он получит уведомление как раз в 9 утра.

      Нет. Если интересно разобраться, почему так, пожалуйста, прочитайте ещё раз соответствующий фрагмент статьи, я всё подробно описал :-)

      Вообще, выражение «перевод времени на сервере» не имеет никакого смысла. Время на сервере (и на любом другом компьютере — ноутбуке, планшете, телефоне) можно только синхронизировать с сервером точного времени. Переводится время для пользователя, который на данный момент работает в ОС и для которого ОС, согласно настройкам пользователя, переводит внутреннее время в локальное время пользователя. И вот во время этого перевода уже учитывается перевод часов. Нет, конечно, есть исключения, однако мы же говорим о правильных ОС и правильной работе со временем, правда?
  • +1
    Вспоминается видео, а также всякие весёлые вещи типа «эпоха макинтош vs эпоха unix». Также вспоминаются причины, по которым тип datetime2 поддерживает даты только «January 1,1 AD through December 31, 9999 AD», а datetime и вовсе «January 1, 1753, through December 31, 9999».
    • +2
      Да, ссылку на это шикарнейшее видео я давал в своём переводе, к которой аппелирует данная статья. Вообще, конечно, про работу со временем можно говорить бесконечно. Столько всего «прекрасного» придумывают люди, чтобы страдать! :-)
  • +1
    А если дату и время рассчитывать на клиенте (в браузере), а на сервер всегда отправлять в UTC? В таком случае вроде проблем быть не должно? Хотя, конечно, такой вариант не подходит для ряда сценариев, включая ваш сервис напоминаний из примеров.

    И спасибо за отличную подборку примеров. Может быть сделать какой-то чек-лист непривычных ситуаций, которые стоит держать в уме при работе с датами?
    • 0
      Насколько я вообще знаю, сложная работа со временем в JS просто невозможна нативными средствами. Например, нельзя однозначно определить таймзону пользователя, можно только получить общую информацию — смещение относительно UTC, включен ли переход на летнее время и т.д. И по этой информации попытаться угадать часовой пояс (что будет просто пальцем в небо). Учитывая огромное количество компьютеров со старыми версиями ОС (пресловутый XP), а так же компьютеров, пользователи которых тупо не ставят обновления (в которых приходят новые настройки часовых поясов), и при проблемах с часами просто переводят их руками, это грозит катастрофой.

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

      Я могу ошибаться, я не так хорош в JS, как в других вещах, если я не прав, буду рад услышать замечания :-)
      • +1
        Для JS есть библиотека Moment.js
        Она умеет преобразовывать даты, в том числе учитывая тайм-зоны. Причем у них есть довольно большой файл для локализации и учета часовых поясов. Но да, согласен, неверные часовые пояса пользователей с легкостью все испортят.

        И еще быстрое гугление подсказало мне библиотеку ical.js для парсинга формата iCalendar.
  • +1
    Хорошая статься.

    Можете представить себе сколько проблем с этими тайм зонами в системах резервации авиабилетов и управления взлетами.
  • –4
    Мне кажется вы немного не теми понятиями оперируете, и поэтому все выглядит так сложно. Гораздо проще оперировать понятием timestamp — количество миллисекунд с 1 января 1970 года 00:00 UTC. Это с точностью до миллисекунды соответствует определенному моменту времени во вселенной, не зависимо ни от каких таймзон. Тем более почти все СУБД и языки представляют дату в памяти именно так. Сравнивать timestamp-ы между собой так же просто как числа, тут тоже не нужны никакие таймзоны.
    Таймзоны становятся нужны когда вам нужно отобразить дату для пользователя, получить дату от пользователя, оперировать частями даты (час, минута, день, месяц, год, и т.п.). В таком случае есть 2 подхода:
    1. Вы можете использовать везде одну таймзону (серверную), сказать пользователю что все отображается в этой таймзоне, все операции в запросах БД и в программе по умолчанию оперируют в серверной таймзоне, телодвижений практически не требуется.
    2. Где-то хранить разные таймзоны, они могут быть привязаны к пользователю, могут быть привязаны отдельно к каждому полю с датой в БД. Благо многие СУБД и языки поддерживают типы данных timezone и timestamp with timezone.
    Сравнивать timestamp-ы с разными таймзонами все так же просто, внутренний timestamp ведь одинаков, это все еще абсолютный момент времени, просто с дополнительной информацией о смещении. Для отображения, конвертации из строки, операций с частями даты как правило существует полный набор встроенных средств в СУБД или в библиотеку языка программирования. Все переходы на летнее/зимнее время, смены часовых поясов обрабатываются автоматически, обновления приходят с обновлениями ОС, СУБД или библиотек, так что при правильно написанном коде заботиться об этом практически не нужно.
    • +3
      Timestamp — это то же время в UTC, просто для удобства работы с ним представленное не в виде объекта, а в виде числа. «Timestamp with timezone» — вообще другое. Попробуйте перевести время «2014-10-26 01:30:00» в таймзоне «Europe/Moscow» в UNIX-timestamp?

      Мне кажется, вы не читали статью, простите за грубость.
      • 0
        Попробуйте перевести время «2014-10-26 01:30:00» в таймзоне «Europe/Moscow» в UNIX-timestamp?

        jsfiddle.net/rq9vwobz/
        Тут используется js библиотека moment.js. Для даты в таймзоне 'Europe/Moscow' и в UTC таймзоне внутри хранится один timestamp (по прежнему один момент времени), но разный offset, который влияет на отображение.
        • 0
          Как вы прокомментируете вот это? jsfiddle.net/k2hL2s91/
          • 0
            Москва перешла с UTC+4 на UTC+3, соответственно через секунду после «2014-10-26 01:59:59» стало опять «2014-10-26 01:00:00», далее прошел еще час (3600 секунд) и стало «2014-10-26 02:00:00», что в итоге и дало 3601 секунду в вашем примере. Так как мы имеем одно и то же строковое представление времени для разных моментов времени («2014-10-26 01:00:00» до и после перевода часов), библиотека не может понять какой именно момент вы имеете ввиду и берет например более ранний, что должно быть задокументированно.
            Также бывает что из-за перевода часов вперед для какого-то на первый взгляд корректного строкового представления времени нету определенного момента времени (этот час просто пропустили, перевели стрелки), тогда хорошая библиотека должна сообщить об ошибке.
            • 0
              Мне кажется вы немного не теми понятиями оперируете, и поэтому все выглядит так сложно. Гораздо проще оперировать понятием timestamp

              То есть UNIX-timestamp === UTC, следовательно всё, что я писал в статье — актуально (если заменяем в тексте «время в UTC» на «timestamp», ничего не поменяется).
              • –2
                Согласен, но в таком случае некоторые моменты выглядят не очень логично, нарпимер
                Если вам нужно хранить время в прошлом или в будущем, сохраняйте локальное время пользователя, а рядом сохраняйте его таймзону. А ещё лучше, так, чтобы наверняка, сохраняйте географическое положение пользователя. Если нужно делать выборки по этому времени, сохраняйте рядом время в UTC, и обновляйте это время при изменении информации о часовом поясе.

                Это совершенно излишне, лучше использовать timestamp with timezone, который выражаясь в ваших понятиях хранит «время в UTC» и таймзону. Преимущества я уже называл, сравнивать с другими timestamp with timezone-ами очень легко, ничего не надо конвертировать, просто сравнение двух чисел. Географические координаты тут скорее всего не пригодятся, так как именованные таймзоны (не аббревиатуры вроде MSK, MSD, а «Europe/Moscow») уже привязаны к географическому месту.

                И вещи вроде «перевод в UTC», «перевод в локальную таймзону» смысла не несут, всегда хранится timestamp и опционально таймзона, которая используется для некоторых операций, в таком случае конвертация не нужна в принципе. Мы просто применяем таймзону к timestamp-у.

                Я не говорю что что-то неверно в вашей статье, я говорю что представлять дату не в виде строки или объекта (как в вашей статье), а в виде числа гораздо удобнее и проще для понимания.
                • +4
                  Географические координаты тут скорее всего не пригодятся, так как именованные таймзоны (не аббревиатуры вроде MSK, MSD, а «Europe/Moscow») уже привязаны к географическому месту.

                  Пример с Читой очень хорош и показателен.

                  лучше использовать timestamp with timezone, который выражаясь в ваших понятиях хранит «время в UTC» и таймзону

                  Всё так и есть. Если база данных позволяет хранить дату-время с таймзоной (как, например, postgresql), то я, навскидку, не вижу никаких проблем. При условии, что мы своевременно обновляем таймзону в базе данных и при условии, что таймзоны в коде абсолютно идентичны таймзонам в базе данных, и при условии, что обновление таймзон в базе данных происходит единовременно с обновлением таймзон в коде на всех серверах проекта (которых может быть тысячи).

                  Наверное, следовало написать об этом в статье, чтобы этот момент не вызывал столько вопросов. Суть ведь вовсе не в том, в каком поле мы храним информацию, а в том, в каком виде мы её храним. Одно поле «timestamp with timezone» или два поля «timestamp» + «timezone» — всё это особенности реализации конкретного проекта и вы, порой, бываете сильно ограничены в выборе тех или иных инструментов, той или иной базы данных. В любом случае, замечание резонное, принимается.

                  Представлять дату в виде строки, а не числа, проще для человека. Представлять дату в виде объекта (а не числа) проще при работе с датами в коде (получить номер месяца, день недели и прочие штуки). Впрочем, это уже совсем другая история :-)
    • +1
      Это с точностью до миллисекунды соответствует определенному моменту времени во вселенной

      Посмотрите, всё-таки, видео.
      • 0
        Посмотрел. Противоречий с тем что я написал не нашел. Если вы про leap second — с этим как я понял пока вообще все сложно, мало где она поддерживается. И наличие leap second никак не противоречит концепции timestamp как количества миллисекунд с определенного момента времени. Просто преобразование из timestamp в форматированную дату должно будет поддерживать историю изменений leap second.
      • 0
        Длительность секунды (а следовательно и миллисекунды) строго константна, и определена как 9192631770 периодов излучения атома цезия-133, так что свое утверждение я считаю вполне обоснованным.
        Вот длительность минуты из-за leap second может различаться, что безусловно создает массу проблем.
        • 0
          Не всегда :-) www.youtube.com/watch?v=-5wpm-gesOY#t=555 — вот с этого момента рассказывается про идею «размазывания» корректировочной секунды в течение всего дня.
          • 0
            Да, интересная вещь. Но это скорее на очередной костыль похоже, который просто чисто технически приносит меньше неприятностей. UTC основан все-таки на атомной секунде. И в идеале программная реализация UTC должна знать что между «2012-06-30 23:59:59 UTC» и «2012-07-01 00:00:00 UTC» прошло 2 секунды.
  • +3
    Да, 21 июля 2014 года Государственная дума Российской Федерации приняла законопроект об отмене летнего времени. Согласно этому закону, с 26 октября 2014 года, смещение для таймзоны Europe/Moscow стало «UTC+3» вместо «UTC+4» (а ещё переход на летнее время отменили, но речь сейчас не об этом). Соответственно, если мы отправим уведомление пользователю 3 ноября в 5:00 утра по UTC, он получит его в 8:00 утра по Москве, и я уверен, что пользователь будет недоумевать, ведь он просил, чтобы уведомление пришло ему ровно в девять утра.

    Только вы забыли, что 26 октября 2014 года стрелки часов будут переведены на 1 час назад, и таким образом 9:00 3.11.2014 станут 8:00 3.11.2014. Если приводить время к UTC, то тут будет все логично: 3.11.2014 9:00 +004 → 3.11.2014 5:00 +000 → 3.11.2014 8:00 +003

    Теперь вы делаете предположение, что пользователь будет негодовать. Рассмотрим его подробнее.

    Предположим, что пользователь ставит будильник за 1 час, чтоб не проспать на работу к 10:00, тогда, действительно, он проснется на 1 час раньше и будет негодовать.

    А теперь предположим, что пользователь принимает лекарства: 1 таблетку каждые 10 дней (ошибка ± 10 минут смертельна). Предыдущую таблетку он принял 24.10.2014 9:00 +004. Следующую таблетку ему надо принять ровно через 10 дней: 3.11.2014 8:00 +003. В этом случае пользователь будет бесконечно рад, что «умный» календарь не сдвинул напоминание на 1 час вперед.

    Понимаете к чему я клоню? Нельзя решать за пользователя, что для него важно, а что нет.

    В статье вы пытаетесь «зашить» бизнес-логику в формат хранения данных. Приведение времени события к UTC или хранение тайм-зоны — достаточно. Все остальное можно (или даже нужно) делать в алгоритмах обработки.

    С уважением, бывший разработчик календаря Onlyoffice (former Teamlab).
    • +1
      Ничего из того, что вы написали, не противоречит моей статье :-)

      Вот моя основная мысль (позволю себе процитировать её ещё раз):
      Вывод можно сделать простой и совершенно банальный: никогда не слушайте категоричных утверждений, рекомендующих вам никогда не делать тех или иных вещей. Всегда продумывайте и прорабатывайте все возможные варианты, особо внимательно прорабатывайте архитектуру проекта, пишите качественные тесты и поддерживайте их в актуальном состоянии.

      Случай с таблеткой настолько уникален и вырожден, что я не могу придумать ни одного подходящего варианта из реальной жизни. И я не слышал вообще ни одной жалобы от пользователя относительно того, что все события в календаре остались на своих местах (по локальному времени), хотя полгода назад он задавал их в UTC. Понимаете, к чему я клоню? Подавляющее большинство пользователей ожидает, что если они поставили событие на 10 утра, оно произойдёт в 10 утра, даже если поменяется часовой пояс.

      Мы не можем оправдываться перед пользователями тем, что они должны были знать про закон о переводе часов, должны были зайти в наш календарь и сами должны были передвинуть все события после 26 октября на час вперёд. Если мы скажем им, что мы не могли решать за них, что важно, а что нет, это будет выглядеть совершенно глупо, и в таком случае я буду первый, кого уволят с работы и я тоже буду «бывшим разработчиком календаря».

      Кстати, из-за странного бага в связке iCal у нас в некоторых случаях некоторые события сдвинулись назад (iCal переписал их поверх старых с той датой, которая, как он думал, была правильная). И вот тут-то каждый пользователь счёл своим долгом пожаловаться нам, что мы — уроды, и не можем сделать нормальный календарь, что нам следовало бы следить за законами о переводах часов и что нас всех следует уволить.

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

      В статье вы пытаетесь «зашить» бизнес-логику в формат хранения данных.
      Нет.

      Приведение времени события к UTC или хранение тайм-зоны — достаточно.
      Да нет же =) Ну прочитайте же статью, я ведь написал там про то, что произойдёт, если мы будем хранить время в UTC, и почему этого недостаточно :-)

      С уважением к коллеге, Владимир.
      • +2
        Пользователь может переехать в другую часть света, будете ли пересчитывать напоминалки в календаре на его локальное время? Напоминалка может быть привязана польщователем к чему-то локальному (будильник), а может и к чему-то глобальному (совещание по скайпу с коллегой из другого города)
        • +2
          Это совсем другие вопросы. Возможно, будем пересчитывать, да, всё зависит от задачи. Может, не будем (если найдётся другое приемлимое для, пардон, хайлоада, решение).

          Главное, чтобы пользователь получилот нас то, чего он ожидает.

          Главная тема моей статьи — опровергнуть высказывание «всегда храните время только в UTC и всегда работайте со временем только в UTC». Что бывают случаи, когда время в UTC всё ломает, хотя в подавляющем большинстве случаев можно спокойно использовать UTC и вообще не иметь никаких проблем. Мне кажется, у меня это получилось :-)
        • 0
          Кейс со скайпом весьма актуален.
      • 0
        Прочитал 2 раза. Все кейсы, что вы рассматривали, я смог бы решить с помощью приведения времени событий к UTC для хранения, и хранения тайм-зоны пользователя, как свойство в профиле.

        От себя бы добавил нулевой совет:

        0) Никогда, ни при каких обстоятельствах не храните/обрабатывайте/etc. время событий в локальном формате (т.е. без явного учитывания тайм-зоны, либо UTC).

        В любой момент времени вашим сайтом/сервисом/скриптом/библиотекой/… могут начать пользоваться люди из другой страны или тайм-зоны, и тогда вам придется переделывать существующее, вероятнее всего, применяя «костыли», вместо движения вперед.
        • 0
          Спасибо! Про нулевой совет — полностью согласен, так и делаем :-)

          Если все рассмотренные мною сценарии можно привести к хранению времени события в UTC (плюс хранению таймзоны пользователя), то как бы вы решили самый первый случай с учётом того, что произошло на самом деле (изменение таймзоны «Europe/Moscow»)? Мне правда интересно, я для того и написал статью, чтобы обсудить эти вопросы с умными людьми.
          пользователь из Москвы зашёл на наш сайт 2 марта 2014 года и создал напоминание на 09:00 утра 3 ноября 2014 года
          При условии, что получить напоминание он должен в 9:00 утра 3 ноября 2014 года по Москве.
          • +1
            Я бы так решил:

            После смены тайм-зоны для всех пользователей, кого это затрагивает (информация из профиля), вывести уведомление (или вариации — email/...), что произошла смена тайм-зоны, которая затрагивает созданные ими события (список), и если это интерактивный сеанс, то предложить варианты: (default) сдвинуть время событий, либо оставить как есть, либо пересоздать события со сдвигом времени (если повторяющиеся),… варианты.
            • 0
              На мой взгляд, это не решение проблемы, а попытка залечить симптомы. Попытался навскидку написать, почему я не принял бы такое решение в живом проекте:

              1. Но зачем что-то спрашивать у пользователя, если в данном конкретном случае (Europe/Moscow) можно сделать всё совершенно незаметно для него?

              2. Что произойдёт, если в момент сдвига времени событий упадёт программа/скрипт и часть событий будут сдвинуты, а часть — нет?
              — Повторно сдвигать? Как узнать, что сдвинуто, а что — нет?
              — Не сдвигать? Тогда часть данных будет не обработана.

              3. Пересоздавать события не всегда вариант (либо локи на записи, либо пользователь может не получить своих данных), опять же, что делать, если что-то упадёт?

              4. В случае, если у нас огромная база данных (сотни гигабайт только таблица с событиями) и если все пользователи начнут сдвигать свои события одновременно, у нас будут проблемы. «Специальный скрипт», о котором я говорил в статье, сделает это аккуратно, постепенно и равномерно для всех событий подряд, сам заботясь о нагрузке на базу. Более того, его можно запускать повторно, ведь оригинальные данные не меняются.

              5. Такая схема работы очень плохо тестируется.

              В общем, простите, но в определённых случаях (надёжный, нагруженный проект) такой вариант нежизнеспособен и грозит порчей данных, хотя я вполне допускаю приемлимость такого решения для большинства сервисов в большинстве ситуаций.
              • 0
                1. Но зачем что-то спрашивать у пользователя, если в данном конкретном случае (Europe/Moscow) можно сделать всё совершенно незаметно для него?

                В моем понимании — это ключевой вопрос. Пользователь должен знать, что его события изменились. У пользователя должен быть выбор.

                Все остальное решается транзакциями, очередями и пр. В настоящее время существует много технических решений.

                И ещё хочу обратить внимание — сам характер бизнеса и стоимость решения могут накладывать ограничения.
                • +2
                  Пользователь должен знать, что его события изменились. У пользователя должен быть выбор.

                  В том-то и дело, что они вообще не изменились :-) И я не согласен про «пользователь должен знать», но тут вообще два разных равноправных подхода, и смысла спорить, наверное, нет.
  • +1
    Ещё три подводных камня:
    — Нарушение временного континуума в момент перевода часов (обсудили чуть выше)
    Система дат 1904
    Секунда координации

    Кстати, самый рейтинговый ответ в Stack Overflow посвящён именно часовым поясам. Джону Скиту уже пришлось редактировать свой ответ 2 раза из-за того, что часовые пояса продолжают меняться…
    • +1
      То есть один из самых рейтинговых. Есть ещё круче.
    • 0
      Спасибо за ссылки! Отличное дополнение к статье :-)

      Я не пытался составить полный перечень проблем, просто хотел обратить внимание, что бездумное следование чьим-то советам может привести к страданиям. Хотел на примерах показать некоторые из подводных камней.
  • 0
    Всё правильно, со временем надо быть осторожным.

    Ещё один пример казуса с временными зонами:
    ДНР (Донецк) решила, что у них в Донецке московское время. Украина (Киев) решила, что у них в Донецке время на час меньше Московского. На одной и той же территории.

    Задача: если в календаре стоит «будить каждый день в 13 часов на обед» — когда должен сработать будильник? На работе у сепаратиста время московское, дома бендеровка-тёща поставила киевское. В выходные он дома, в рабочие дни на работе. В форумах будет та же проблема — что бы он не поставил, время будет правильное только с какой-то вероятностью, в зависимости от того, на чьём компьютере заходить на форум.

    Украина вообще набор казусов — РЖД недавно прекратила продажу билетов на Украину, так как не смола справиться с временными зонами:
    «В связи с отсутствием окончательного решения о переходе Украины в марте 2013 года на летнее время РЖД вынуждено приостановить продажу проездных документов на поезда дальнего следования, в том числе прицепные и беспересадочные вагоны в сообщении c Украиной, отправлением с территории России с 30 марта 2013 года»

    С постгресом тоже не всё здорово. EnterpriseDB недавно вовремя не обновила для windows дистрибутива файл зон, и время считалось неправильно.
    • +2
      Юридически никакого ДНР нет и не предвидится, поэтому я бы не стал ориентироваться на «принятые» там законы.
      • +1
        При чём тут юридичность или законы? Мы говорим о людях. А никакие юридические законы ничего не говорят о том, что у пользователя на компьютере за временная зона. Что он захочет — то и будет.
  • 0
    Ну так и чем ваш вывод отличается от вывода Tom Scott?
    What you learn after dealing with timezones is that what you do is you put away your code. You don't try to write anything to deal with this. You look at the people who've been there before you. You look at the first people, at the people who dealt with it before, the people who built a spagetti code. You go to them and you thank them very much for making it open source. You give them credit and take what they made. You put it in your program and you never ever look at it again because that way lies madness.
    • 0
      Если вы прочитали только «TL;DR», то ничем. Если вы прочитали всю статью — то всем. Например, тем, что Том рассказывал о работе с таймзонами руками, без библиотек и в конце пришёл к выводу о том, что нужно использовать библиотеки и базы данных таймзон. Я же рассказываю о том, какие могут быть проблемы, если мы будем использовать эти самые библиотеки и базы данных таймзон. Развитие, дополнение, продолжение, так сказать, рассказа Тома :-)
  • +2
    Люди, хранящие время про будущие события, закладываются на очень шаткий фундамент: они предполагают, что госдума не будет менять число часов в сутках, количество дней в неделе, порядок следования натуральных чисел и понятие «завтра». Но это ведь только до тех пор, пока не окажется, что с течением времени дети стареют и умирают, так что для защиты детей от разрушительного воздействия времени следует принять закон о запрете.

    Не?
    • +1
      Не
  • +1
    Есть еще как минимум два прекрасных сценария, когда работа с часовыми поясами доставляет боль и страдание.

    1. Время в системе хранится в локальном часовом поясе пользователя с указанием таймзоны. Наступает 26 октября 2014 года, на часах 1:30 ночи. Какое это время в UTC? Совершенно непонятно, потому что в этот день время с часу до двух ночи повторяется дважды, а UTC течет непрерывно. В любой системе, которая вынуждена оперировать с несколькими часовыми поясами одновременно возникает неопределенность, которую нужно решать явно, тем или иным способом.

    2. В календаре отмечена регулярная (еженедельная, например) встреча между двумя людьми, находящимися в разных часовых поясах, например, в Москве и Киеве. Для проведения встречи и там и там нужны драгоценные ресурсы переговорных комнат. И вот наступает момент, когда Москва отказывается от перевода часов на зимнее время, а Киев часы переводит. В одном из городов обязательно произойдет смещение времени начала встречи на час — и она пересечется с другими в этой же переговорке, которые были созданы без участия второго города.

    Полностью поддерживаю автора, работа с часовыми поясами — это боль и страдание, и очень редко встречающийся навык, что тоже, иногда доставляет неудобства при использовании в работе сторонних библиотек.
    • +1
      В случае с переговорками их нужно бронировать в UTC. а в случае внепланового изменения часовых поясов уведомлять всех забронировавших, что локальное время у кого-то из участников «поехало».

      Впрочем, вряд ли кто-то бронирует переговорки более чем за месяц, а обновление базы часовых поясов было доступно заранее.
      • +1
        Речь о повторяющихся встречах. Такую могли создать и год назад, но она по-прежнему проводится. Проблема даже не в том, что у участников поедет время — это, как правильно замечено, легко решается рассылкой уведомлений. Проблема в том, что при смещении встречи она начинает пересекаться с уже созданными по локальному времени событиями в этой же переговорке. Расписание переговорок обычно очень плотное, и наложение почти гарантированно.

        Отдельным пунктом можно долго описывать, как в календарях у сотрудников компании, растянутой на пять часовых поясов, организовать еженедельную общую встречу утром каждого понедельника :)
        • +1
          Поэтому я и говорю — бронирование в UTC. Ничего в одной переговорке не наложится. А вот у москвичей в календарях разные встречи из разных источников могут наложиться. Но тут уже сложно что-то придумать, кроме как уведомить.
  • +1
    Ещё из возможных граблей хотел бы отметить день рождения. По опыту его крайне нежелательно хранить в виде datetime, поскольку различные виджеты выбора даты создают время либо как текущее в UTC, либо как 00:00 в UTC. И то и то — очень неудачные варианты. В первом случае все вообще непредсказуемо, во втором случае день рождения предсказуемо смещается на день при любых таймзонах меньше той, где находился пользователь в момент создания такого события.
    • +2
      На этом погорели разработчики ICQ-протокола (там было много версий, за все не скажу, но пришлось столкнуться). Они хранили дату рождения юзера в unixtime (то есть с точностью до секунды зачем-то)

      И совершенно непонятно было — если у человека стоит дата рождения типа 13 марта 01:00 UTC — то у него в России (которая справа от Гринвича) действительно 13 марта. А если человеку повезло родиться в Америке (слева от Гринвича) — то его надо с днем рожденья принято поздравлять 12 марта вообще-то (когда 13-ое наступило только в восточном полушарии).
      • +1
        Кстати, если уж очень хочется хранить так дату, то лучше ставить время 12:00UTC, ущерб минимизируется.
        • 0
          — In 2009, Samoa moved the International Date Line to the other side of its territory, which means that its time zone was changed from GMT–11 to GMT+13. It observes GMT+14 in the southern summer.
          — Line Islands of Kiribati observe GMT+14 as standard time.
          — Phoenix Islands of Kiribati, Tokelau, and Tonga observe GMT+13 all year round.
          — Fiji and New Zealand observe GMT+13 in southern summers.

          :-)
          • 0
            Ага, тоже недавно (как пояса обновлял) открыл для себя смещение в +14 часов:)

            Нет, дата рождения — это, к сожалению, именно дата, просто дата. Без таймзоны, без ничего. И каждый раз, когда её надо сделать временем, вокруг неё надо плясать с бубном…
  • 0
    Вовсе не нужно хранить локальное время. Достаточно UTC + инфы о часовом поясе или геопозиции. При изменениях в tzdata просто пробегаться по базе и добавлять нужно смещение.
    • +1
      Вовсе не «не нужно». В статье этот способ описан, описаны его достоинства и недостатки. И про пробегание по базе и обновление смещения тоже написано. Прочитайте, пожалуйста, внимательно?
      • 0
        Если вам нужно хранить время в прошлом или в будущем, сохраняйте локальное время пользователя, а рядом сохраняйте его таймзону. А ещё лучше, так, чтобы наверняка, сохраняйте географическое положение пользователя. Если нужно делать выборки по этому времени, сохраняйте рядом время в UTC, и обновляйте это время при изменении информации о часовом поясе.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка