Как стать автором
Обновить

Комментарии 14

Как раз сейчас решаю подобный вопрос, мне важно сохранять штампы времени по часовому поясу клиента в данный момент, чтобы потом кто угодно и когда угодно мог увидеть, что клиент сделал действие в определенное время по местному времени. К сожалению, все ваши рекомендации по данному поводу для меня не подходят, нужно либо хранить на каждую такую дату в соседнем поле часовой пояс, что неудобно, либо игнорировать часовой пояс и хранить просто штамп, подразумевая что это время на то место и время где клиент совершил действие. Очень жаль, что нет в mysql единого поля datetime + timezonе, можно конечно в строке хранить, но это тоже не вариант.

Предварительно напишу:

Timestamp = time in ms (UTC) from 1970/1/1 (link). Timestamp имеет формат целых чисел, iso-string имеет формат строки. В iso-string можно передать TZ, в timestamp - нет.

Предполагаю, что разговор о логировании действий пользователя системой. Если говорить о таблице в БД, можно предположить:

| id | user_id | action_type | time_utc                         | time_zone       |
-----------------------------------------------------------------------------------
| 1  | 1       | some_action | iso_string or timestamp (one of) | timezone_offset |

В условной функции запроса по энпоинту можно совмещать в обе стороны iso-string+TZ. В любом языке данный функционал имеет место быть.

Если речь об запросах в БД, то возможно было бы удобно создать дополнительное поле user_time в формате timestamp. Это позволит свести расчеты запросов в единую плоскость для конкретных кейсов, но выдавать другой системе для отображения и использования придется всю строку (иначе бытовые расчеты будут невозможны). Приходилось иметь дело с api, в которой очень много неясных систем отсчета, оттуда и родилась статья по унификации.

Не только логирование, я пишу программу для курьерской службы (angular+symfony), у каждой сущности может быть куча полей с датами и временем, десятки (прибытие, убытие, ожидаемое и фактическое и так далее), и всегда надо видеть это относительно того места, где посылка находится. И кстати, если ангуляру отдавать время с таймзоной то date pipe будет менять время при изменении пользователем часового пояса на своем компьютере, что тоже неприемлемо, функции игнора часового пояса у этого пайпа нет, поэтому передавать с бэка в ангуляр таймзону тоже не вариант.

Пробовал вторым аргументом передавать в пайп таймзону?

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

Возможно для данного кейса это и будет решением, но звучит довольно дико.

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

Игнорировать TZ может быть очень опасно в будущем, если, кончено, приложение будет реально использоваться в коммерции.

Могу предложложить сделать так:

  • у юзера в настройках задается тайм зона, изначально берущаяся из часов

    • в приложении будет очевидная единая тайм зона, которую можно менять при желании

    • можно переопределять TZ автоматически при заходе юзера в приложения (добавить пункт "автоматически" в настройки юзверя)

  • эта тайм зона будет использоваться для вставки в angular pipe вторым аргументом

    • скорее как бонус

  • на сервер будет отправляться время с TZ, но в базе сохраняться время UTC

    • соблюдаем единую шкалу измерений

  • при необходимости, можно сохранить изначальную TZ в качестве сдвига в соседнем столбце

    • сдвиг будет легко вычислить

  • при обмене данными FE-BE использовать iso-string, при получении на BE можно (при необходимости, например, для расчетов) использовать перевод в timestamp UTC

    • это позволит применять расчеты в фильтре запроса к БД; все данные в единой шкале изменений и не будет проблем в вычислениях

Итого:

  1. Вычисления доступны, все данные в одной системе исчеслений

  2. Структура простая и очевидная

  3. Структура удобна для фронта и бэка, формат унифицирован

Зачем сохранять штампы времени по часовому поясу клиента? Храните в UTC, он однозначно преобразовывается в другую таймзону, просто делайте преобразование перед показом клиенту.

Есть нюанс для повторяющихся событий с переходом на зимнее/летнее время.
Например, клиент с летней таймзоной UTC +3 (у которой есть переход на зимнее/летнее время) добавил напоминание, которое должно ему отсылаться каждый день в 10 утра. В базе сохранилось в UTC 0 (7 утра). И всё будет хорошо до перехода на зимнее время, так как у клиента тайзона станет UTC +2 и ему напоминание придет в 9 утра, а должно в 10.

А как вы различаете две ситуации: "у клиента сменилась таймзона из-за перехода на зимнее время" и "у клиента сменилась таймзона из-за переезда в соседний часовой пояс"?

Иногда может быть необходимо показать время какого-то события в том часовом поясе, в котором оно произошло, а не в том, в котором находится клиент.

То есть база данных хранит в UTC, человек сидит во Владивостоке, и читает новость, в которой временная метка вида "в 12 часов московского времени ..."

Вот ваш третий пункт:

"+180" = "+03:00" = TZ Москва

на мой взгляд, потенциально проблемный. Хотя бы потому, что "+03:00" может означать не только Москву, но и Турцию, Белоруссию, Ирак и т.д. - и в этих странах могут быть свои нюансы по переводу зимнего/летнего времени, срокам действие этих переводов и пр.

Поэтому хранить временную зону только в виде смещения - крайне рисковано. Хранить в виде трёхбуквенной аббревиатуры (MSK) тоже нереально, та же EST подходит и под UTC+10 и UTC-5 и UTC+2

Я у себя храню дату/время с временной зоной в классе, с тремя публичными свойствами - "Время в UTC" (DateTime с DateTimeKind.Utc), "Временная зона" (string - "Europe/Moscow") и "Время в этой временной зоне" (DateTime с DateTimeKind.Unspecified).

При этом запись зоны в виде такой строки вполне понятна и при компиляции приложения под linux и под windows (при установленноых библиотеках Microsoft.ICU.ICU4C.Runtime) ну и в БД хранить проще :)

Относительно хранения "Europe/Moscow". Та же библиотека MomentJS (самая популярная библиотека для работы со временем в JS/TS) имеет подбиблиотеку по работе с TZ. В ней есть работа с данными в формате "Europe/Moscow". В ответ функция вернет TZ. Хороший вариант для хранения.

На практике данный подход не применял, но если есть условный словарь для расшифровки (поддерживаемый кем-то), то будет хорошим вариантом для работы со временем.

В этом случае думаю, что хранение "Время в этой временной зоне" будет излишним.

-------------------

Не продумывал смещение времени относительно текущей геопозиции пользователя, но не думаю, что это является реальным кейсом. Я бы сказал, что тут нюанс правильной интеграции системы бэка и фронта. Считаю что много тут зависит от ТЗ.

Кейс 1. Допустим у нас есть необходимость создания типичного отображения времени подачи заявки на бэк. Чтобы не добавлять геополитическую логику в данную систему, я бы оставил это на стороне фронта:

  1. Фронт получает ISO-string UTC (00:00) и смещение времени по сохранены данным

  2. Пользователю отображается время подачи заявки И временной пояс подачи заявки

  3. Да, могут быть заявки в техподдержку из-за непонимания юзверя, но тут можно дополнять оповещение времени тултипом и др элементами UI/UX. Так что задача уже не программиста как таковая, а дизайнера.

Кейс 2 - отображение времени отправления поезда. Тут следует отменить, что существует шкала времени железной дороги в каждой стране и она отмечается по одному городу (смещение времени считается от него), но пока забудем об этом.

В этом кейсе также фронт сам разруливает что отображать:

  1. Фронт получает ISO-string UTC и смещение времени относительно пункта отправления/прибытия

  2. Фронт отображает время, совмещая ISO-string с полученным смещением

  3. Фронт отображает сноску "время местное"

Тут есть нюанс, что отправление и прибытие могут быть в разных часовых поясах и датах. Если есть желание не раздувать логику на бэке и обработать данные на фронте, то данные однозначно должны быть с указанием времени TZ местного времени для просчета (будет это ISO-string+TZ или ISO-string и смещение отдельными параметрами - не важно).

Кейс 3 - сервис аналитики. Допустим нам нужно получать данные, обрабатывать их и выдавать статистику. Тут стоит отметить, что стоит уделить отдельное время на компановку задачи. В обычном понимании оператора, скорее всего, нужно будет отображать реальное время пользователя у пользователя. Тут нам и поможет смещение времени, переданное от него.

  1. Клиент что-то творит, предаются данные в формате ISO-string с TZ.

  2. Бэк разделяет данные на UTC и смещение

  3. Фронт аналитики получает ISO-string и смещение времени

  4. Фронт аналитики совмещает данные и выводит, например, график, с реальным временем пользователя

  5. Сноски тут не понадобятся

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

Насчёт "условного словаря для расшифровки" - первоисточником, разумеется, являются законодательные акты государств, но едной базой куда все они рано или поздно стекаются (и откуда потом растекаются по различным библиотекам) является IANA TimeZone Database - https://www.iana.org/time-zones, либо можно посмотреть в сторону https://cldr.unicode.org/

Ценная информация, спасибо

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации