Комментарии 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
это позволит применять расчеты в фильтре запроса к БД; все данные в единой шкале изменений и не будет проблем в вычислениях
Итого:
Вычисления доступны, все данные в одной системе исчеслений
Структура простая и очевидная
Структура удобна для фронта и бэка, формат унифицирован
Зачем сохранять штампы времени по часовому поясу клиента? Храните в 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. Допустим у нас есть необходимость создания типичного отображения времени подачи заявки на бэк. Чтобы не добавлять геополитическую логику в данную систему, я бы оставил это на стороне фронта:
Фронт получает ISO-string UTC (00:00) и смещение времени по сохранены данным
Пользователю отображается время подачи заявки И временной пояс подачи заявки
Да, могут быть заявки в техподдержку из-за непонимания юзверя, но тут можно дополнять оповещение времени тултипом и др элементами UI/UX. Так что задача уже не программиста как таковая, а дизайнера.
Кейс 2 - отображение времени отправления поезда. Тут следует отменить, что существует шкала времени железной дороги в каждой стране и она отмечается по одному городу (смещение времени считается от него), но пока забудем об этом.
В этом кейсе также фронт сам разруливает что отображать:
Фронт получает ISO-string UTC и смещение времени относительно пункта отправления/прибытия
Фронт отображает время, совмещая ISO-string с полученным смещением
Фронт отображает сноску "время местное"
Тут есть нюанс, что отправление и прибытие могут быть в разных часовых поясах и датах. Если есть желание не раздувать логику на бэке и обработать данные на фронте, то данные однозначно должны быть с указанием времени TZ местного времени для просчета (будет это ISO-string+TZ или ISO-string и смещение отдельными параметрами - не важно).
Кейс 3 - сервис аналитики. Допустим нам нужно получать данные, обрабатывать их и выдавать статистику. Тут стоит отметить, что стоит уделить отдельное время на компановку задачи. В обычном понимании оператора, скорее всего, нужно будет отображать реальное время пользователя у пользователя. Тут нам и поможет смещение времени, переданное от него.
Клиент что-то творит, предаются данные в формате ISO-string с TZ.
Бэк разделяет данные на UTC и смещение
Фронт аналитики получает ISO-string и смещение времени
Фронт аналитики совмещает данные и выводит, например, график, с реальным временем пользователя
Сноски тут не понадобятся
Разумеется, всё зависит от задачи. В моём случае варианта хранения времени только в UTC было недостаточно. А с отображением, всё зависит от кейсов, как в ваших примерах.
Насчёт "условного словаря для расшифровки" - первоисточником, разумеется, являются законодательные акты государств, но едной базой куда все они рано или поздно стекаются (и откуда потом растекаются по различным библиотекам) является IANA TimeZone Database - https://www.iana.org/time-zones, либо можно посмотреть в сторону https://cldr.unicode.org/
Единый формат времени для приложения