Comments 39
momentJs не спасал, точнее он спасал, но только до фронтэнда, для отображения, дальше DateTime по мере путешествия БД > бэкэнд > фронтэнд > бэкэнд > БД менялся и не соответствовал тому-же значению в БД.
Сделали так: из БД брали DateTimeOffset, отдавали его во фронт, там он превращался в объект с двумя полями, одно поле тот же offset (например "/Date(1487688845183)/") которое улетало обратно как параметр, а другое уже отображаемая дата.
DateTimeOffset после всех сериализаций и десериализаций не менялся, и точно соответствовал тому, что хранилось в БД.
Если форматировать не удобно, кидайте в миллисекундах (если в 1 секунду может произойти несколько действий, и нужно выяснить что было раньше).
Отличный ресурс https://currentmillis.com/
Самый надежный и хороший подход — это хранить DateTimeOffset, и работать с ним как на сервере, так и на клиенте. Использовать свой формат сериализации (возможно, с фоллбэком в часовую зону по умолчанию, для старых клиентов).
Все остальное, включая хранение UTC, это полумера. UTC решает проблему сравнения дат, но не всегда помогает правильно их отображать.
Вообще, работа с датами — тема довольно неочевидная и с множеством подводных камней, особенно если в некоторых случаях нужны только даты без времени. Нужно всегда (как отметили уже выше) понимать, что именно нужно показать: дату события в часовом поясе пользователя, дату, как ее ввел пользователь или еще что-то.
Выбор между типами DateTime, DateTimeOffset, TimeSpan и TimeZoneInfo
Структура DateTime подходит для приложений, которые:
…
работают только с датами и временем в формате UTC;
…
Тип DateTimeOffset включает все функциональные возможности типа DateTime, а также сведения о часовом поясе.
На правах личных предпочтений.
UTC DateTime
позволяет сравнивать даты из разных часовых поясов, и отображать даты в часовом поясе пользователя. Это дает общую относительную систему координат, и часто этого достаточно.
Но иногда возникают задачи, когда требуется узнать дату и время в системе координат пользователя.
Например, иногда важна именно выбранная дата (без времени), и она может отличатся из-за разницы в часовых поясах. Здесь UTC не поможет, из него нельзя восстановить реально выбранное пользователем значение (без дополнительной информации о часовом поясе, скажем, из профиля).
1) Если вы сохраняете информацию о будущем событии, которое должно произойти именно в 10 утра в Апреле через год во Владивостоке. Сервера все в Лондоне. В течение года правительство решает не переходить на летнее время. В этом случае поможет сохранение в локальном времени.
2) При переходах от зимнего к летнему и обратно у вас может теряться или дублироваться час в UTC. В этом случае помогает наличие информации о смещении времени.
Вообще, чтобы описать конкретный момент во времени необходимо три величины: дата/время + оффсет + название тайм зоны. Если меньше информации, то время приблизительное, как ни крути. Но для бизнес задач может быть достаточным только UTC, бесспортно. Поэтому необходимо смотреть конкретный случай.
у вас может теряться или дублироваться час в UTCТакие скачки происходят как раз для локального времени, так как оно определено как смещение от времени UTC. Реальная проблема заключается в том, что не все правила перевода сохраняются в базах времени. Бывают ситуации, когда перевод из UTC в локальное время может быть выполнен неправильно для достаточно старых дат (несколько лет назад), что может искажать, например, отчёты.
Вообще, чтобы описать конкретный момент во времени необходимо три величины: дата/время + оффсет + название тайм зоны.По первым двум параметрам вопросов нет, а зачем название тайм зоны?
Если углубится, то при высоких требованиях к расчётам разницы между двух дат, в этот список можно добавить и количество добавленных високосных секунд — на данный момент 27.
На счет названия таймзоны. По моим расчетам она нужна именно, чтобы получить эти карты. Офсет в большинстве случаев не нужен, т.к. можно получать локальное из UTC + TimeZoneInfo. Но без офсета будет проблема с переводом в локальное время тех моментов, которые произошли в течение этого DST перехода. Как то так.
Эта информация — не дата, и не интервал, поэтому ни DateTime, ни DateTimeOffset не являются правильными типами для планируемых событий.
Во-первых, тут есть, как минимум, информация о месте события, которую принципиально нельзя кодировать таймзоной, если не хочется проблем в будущем.
Во-вторых, «через год» — это плохо определённое понятие. Например, если сегодня 28 февраля, то что такое «через год» — 27 февраля или 1 марта? То же самое с понятием «через месяц».
Так что, в вашем сценарии нужно хранить локацию (координаты или код), смещение времени от начала локальных суток, а та часть, что касается будущей даты, должна храниться в виде параметров для алгоритма вычисления этой даты, и их множество не сводится к абсолютной дате или к смещению. И это ещё не вспомнили про рекуррентные события.
В своем примере я имел ввиду, что можно хранить только название таймзоны и date/time (локальный).
var res1 = DateTime.Parse(«2016-03-19T12:17:33Z»)?
var res2 = DateTime.Parse(«2016-03-19T12:17:33-05:00»);
Что бы на выходе у меня было DateTime значение 2016-03-19T12:17:33 в обоих случаях.
В том-то и дело, что если всегда приводить время к UTC при сериализации, указанной вами неконсистентности не будет.
Более того так работаю из коробки стандартный Asp.net Web Api Json форматер.
IEnumerable Get(DateTime startDate)
{
}
я передаю в него
?startDate=2016-03-19T12:17:33Z
и получаю дату смещеную с учетом серверного времени 15:17.
Единственный путь получить правильную дату не передать таймзону вообще — и в этом странность работы дотнета, которая делает небезопасным использование DateTime в качестве параметров API практически всегда.
получаю дату смещеную с учетом серверного времени
Это логично, время приводится к локальному с учетом таймзоны. Тем не менее, оно по-прежнему указывает на ту же самую точку в мировых координатах, которую вы передаете. Вот пример:
var d = DateTime.Parse("2016-03-19T12:17:33Z");
Console.WriteLine($"Value='{d}', Kind={d.Kind}, UTC='{d.ToUniversalTime()}'");
Вывод:
Value='3/19/2016 5:17:33 PM', Kind=Local, UTC='3/19/2016 12:17:33 PM'
Локальное время учитывает +5 сервера, но Kind при этом Local, так что UTC по-прежнему то же. Как вы видите, здесь нет каких-либо манипуляций с оригинальным значением, добавлений часов и т.п.
Ладно, может, сериализация в MVC/API работает по-другому?
public ActionResult Index(DateTime time)
{
return Json(new
{
value = time.ToString(),
kind = time.Kind.ToString(),
utc = time.ToUniversalTime().ToString()
}, JsonRequestBehavior.AllowGet);
}
Ответ:
{
"value": "3/19/2016 5:17:33 PM",
"kind": "Local",
"utc": "3/19/2016 12:17:33 PM"
}
Тем не менее, оно по-прежнему указывает на ту же самую точку в мировых координатах, которую вы передаете.
Нашему приложению вроде бы не нужны были таймзоны, просто локальное время — но они где-то там неявно уже были учтены и скоректировали время во время конвертации фреймворком.
Как мне куда-то это сохранить в базу, а потом востановить правильное время без приведения в UTC — мне же не нужны манипуляции с таймзонами, у нас же простой DateTime?
* единственным и исключительным потребителем информации о времени является тот, кто эту информацию произвёл
* потребителю не нужна однозначность времени.
Т.е., фактически, единственный приемлемый сценарий для хранения локального времени — это хранение времени как строки, в точности как она была введена, без обработки, без использования в расчётах, без передачи этой информации другим клиентам. Только чтобы приватно посмотреть на экране или распечатать. Как только начинаются расчёты, появляются несколько пользователей (сервер, другие клиенты) — немедленно переходить к однозначному представлению в хранилище (т.е. UTC) и к конвертации в локальное время на клиентах (они свои часовые пояса знают лучше, чем сервер).
Скажем, как бы вы решали такую проблему наименьшими усилиями: надо расчитать даты запуска джоба в 10 утра по каждой из нескольких локаций, которые находятся в разных часовых поясах?
По моим расчетам, можно и через UTC: расчитываем последовательность, переводим в локальную, чтобы проверить 10 часов это или нет, сохраняем обратно в UTC.
А пожно просто забить как 10 утра локально и скедулер уже будет вызывать, без конвертаций и расчетов.
В чем видите тут проблему?
Но, в принципе, если у вас ограниченное число хорошо известных локаций и нет проблем обновлять таймзоны по ним, то да, ваше решение тоже работает. Мои замечания больше относятся к системам, которая должны обслуживать неопределённое число клиентов в любой мыслимой и немыслимой зоне.
Нужно хранить именно DateTimeOffset, т.к. при приведении к UTC теряется информация, какое локальное время и смещение были в момент регистрации события.
А эта информация может быть важна — выше привели много примеров.
И кстати, если на машине, где регистрировалось событие, часовой пояс был выставлен неверно, то полная исходная информация поможет выявить ошибку.
Кроме того, зачем вообще терять часть информации на входе, если к UTC всегда можно привести позже, если, к примеру, в отчете нужно показывать время в UTC.
И также верно выше заметили, что если совсем по-хорошему, то кроме смещения нужно хранить и название (или код) тайм-зоны.
Переход от DateTime к DateTimeOffset