Pull to refresh

Comments 39

Вечная проблема DateTime в связке js (angular) <-> asp.net mvc <-> mssql (UTC в datetime), это невозможность точно сказать, какое время будет отображено у пользователя на странице. Чего только не перепробовали и сами форматировали, и использовали сторонние библиотеки, в итоге остановились на DateTimeOffset — и все заработало.
UFO just landed and posted this here
У нас так не получалось. Дата писалась в БД (своя зона) как GETUTCDATE(), вроде бы, что может случится? Но когда мы брали дату из БД как DateTime в бэкэнде (другая зона), и передавали во angularJS фронтэнде (другая зона), то получалось, что мы теряли точное время при преобразовании. А уж когда эта дата возвращалась как параметр обратно в БД (дали клиенту выбор из дат загрузки данных, как пример), то мы точно не попадали в ту дату, которая была в БД.
momentJs не спасал, точнее он спасал, но только до фронтэнда, для отображения, дальше DateTime по мере путешествия БД > бэкэнд > фронтэнд > бэкэнд > БД менялся и не соответствовал тому-же значению в БД.
Сделали так: из БД брали DateTimeOffset, отдавали его во фронт, там он превращался в объект с двумя полями, одно поле тот же offset (например "/Date(1487688845183)/") которое улетало обратно как параметр, а другое уже отображаемая дата.
DateTimeOffset после всех сериализаций и десериализаций не менялся, и точно соответствовал тому, что хранилось в БД.
Рекомендованный подход — принимайте UTC, храните UTC.
Если форматировать не удобно, кидайте в миллисекундах (если в 1 секунду может произойти несколько действий, и нужно выяснить что было раньше).

Отличный ресурс https://currentmillis.com/
Еще, 50 минута а далее.
Fundamental problems that you all know about now - and how to explain them to junior engineers - Jon skeet

В общем случае UTC — не панацея. К примеру, сотрудник, находящийся в командировке, открывая бэкенд своей компании, должен видеть события в локальном времении компании, а не в локальном своем. «Заседание начинается в 15:00, явка обязательна».
UFO just landed and posted this here
А через неделю? В моем проекте так и происходит, что привилегированные юзеры колесят по стране и сами создают и корректируют удаленные события — естественно, в локальном времени места события.
UFO just landed and posted this here

Вы не поняли. Клиент — админ, он сам создает событие, скажем, в 18:00 локального времени места события, т.к. это публичное время (проще говоря, расклеено на афишах). Он оперирует только этим временем вне зависимости от его текущего местоположения.

Самый надежный и хороший подход — это хранить DateTimeOffset, и работать с ним как на сервере, так и на клиенте. Использовать свой формат сериализации (возможно, с фоллбэком в часовую зону по умолчанию, для старых клиентов).


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


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

UTC норм, если придерживаться его на входе и на выходе. Задача правильного отображения передается клиенту, он сам должен знать где он находится, в Канаде, Африке или Австралии, и какое там смещение.

Выбор между типами DateTime, DateTimeOffset, TimeSpan и TimeZoneInfo
Структура DateTime подходит для приложений, которые:

работают только с датами и временем в формате UTC;


Тип DateTimeOffset включает все функциональные возможности типа DateTime, а также сведения о часовом поясе.


На правах личных предпочтений.

UTC DateTime позволяет сравнивать даты из разных часовых поясов, и отображать даты в часовом поясе пользователя. Это дает общую относительную систему координат, и часто этого достаточно.


Но иногда возникают задачи, когда требуется узнать дату и время в системе координат пользователя.


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

Есть несколько случаев, в которых UTC не поможет, даже если все четко с его сохранением и выводом.

1) Если вы сохраняете информацию о будущем событии, которое должно произойти именно в 10 утра в Апреле через год во Владивостоке. Сервера все в Лондоне. В течение года правительство решает не переходить на летнее время. В этом случае поможет сохранение в локальном времени.

2) При переходах от зимнего к летнему и обратно у вас может теряться или дублироваться час в UTC. В этом случае помогает наличие информации о смещении времени.

Вообще, чтобы описать конкретный момент во времени необходимо три величины: дата/время + оффсет + название тайм зоны. Если меньше информации, то время приблизительное, как ни крути. Но для бизнес задач может быть достаточным только UTC, бесспортно. Поэтому необходимо смотреть конкретный случай.
у вас может теряться или дублироваться час в UTC
Такие скачки происходят как раз для локального времени, так как оно определено как смещение от времени UTC. Реальная проблема заключается в том, что не все правила перевода сохраняются в базах времени. Бывают ситуации, когда перевод из UTC в локальное время может быть выполнен неправильно для достаточно старых дат (несколько лет назад), что может искажать, например, отчёты.

Вообще, чтобы описать конкретный момент во времени необходимо три величины: дата/время + оффсет + название тайм зоны.
По первым двум параметрам вопросов нет, а зачем название тайм зоны?

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

Да, относительно UTC вы правы. Я запутался.

На счет названия таймзоны. По моим расчетам она нужна именно, чтобы получить эти карты. Офсет в большинстве случаев не нужен, т.к. можно получать локальное из UTC + TimeZoneInfo. Но без офсета будет проблема с переводом в локальное время тех моментов, которые произошли в течение этого DST перехода. Как то так.
информацию о будущем событии, которое должно произойти именно в 10 утра в Апреле через год во Владивостоке.

Эта информация — не дата, и не интервал, поэтому ни DateTime, ни DateTimeOffset не являются правильными типами для планируемых событий.

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

Во-вторых, «через год» — это плохо определённое понятие. Например, если сегодня 28 февраля, то что такое «через год» — 27 февраля или 1 марта? То же самое с понятием «через месяц».

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

В своем примере я имел ввиду, что можно хранить только название таймзоны и date/time (локальный).
Населённый пункт может переехать из одной таймзоны в другую, если границы зоны изменятся.
Рекомендую всегда брать datetimeoffset. В дотнете много неожиданностей можно отловить с datetime. Например приведение при десериализации. Причина в работе форматеров- если клиент пришлёт datetimeoffset, а параметр типа datetime, время преобразуется с учётом серверной таймзоны. В принципе не понимаю в какой ситуации это может быть ожидаемым поведением.
При сериализации DateTime неожиданностей не будет, если следовать требованию выставлять Kind в DateTimeKind.Utc.
Расскажите как это сделать для следующего кода

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 при сериализации, указанной вами неконсистентности не будет.

у меня js клиент и publlic Web Api — вот приведенное время к UTC по ISO «2016-03-19T12:17:33Z»- если я его десериализирую DateTime.Parse он всегда будет учитывать серверное время и выдавать ожидаемый datetime только если клиент и сервер в одной таймзоне.
Более того так работаю из коробки стандартный 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?
+ это же DateTime тип, какая та же самая точка в мировых координатах? Это тип, который расчитан на работу с локальным временем только, можете расскажите тогда зачем в принципе DateTimeOffset?
Нет, несмотря на то, что в DateTime на самом деле нет TimeZone, установка DateTimeKind.Utc делает его мировым (как признак, без вычислений). Отличие от DateTimeOffiset в том, что последний может вычисляться с любой зоной, в то время как DateTime может быть только локальным или UTC.
Тогда бы я ожидал, что сериализатор бросит ошибку если получит не локальную и не utc таймзону — не поддерживаю таймзону, вместо молча проглотит ее сместит время на пару часов. Универсальное время в мировых координатах надо только в DateTimeOffset. если мне прислали на сервер DateTime в той модели, о которой вы говорите надо всегда быть уверенным, что и клиент и сервер всегда работают в одной таймзоне и не отправят случайно по ошибке неправильно время и всегда вовремя переключаться на DST и обратно. Иначе такая модель приведения времени просто проглотит ошибку, поломает систему и ничего не сообщит о неисправности.
Как определять часовой пояс пользователя? По заголовкам браузера, или просить пользователя указывать его в профиле? Что лучше? Если передавать UTC клиенту и там преобразовывать к локальному времени, то как: javascript или есть другие варианты? А если на сервере формировать, то без знания часового пояса не обойтись…
Хранить в локальном времени можно лишь если выполняются всё нижеперечисленное:
* единственным и исключительным потребителем информации о времени является тот, кто эту информацию произвёл
* потребителю не нужна однозначность времени.

Т.е., фактически, единственный приемлемый сценарий для хранения локального времени — это хранение времени как строки, в точности как она была введена, без обработки, без использования в расчётах, без передачи этой информации другим клиентам. Только чтобы приватно посмотреть на экране или распечатать. Как только начинаются расчёты, появляются несколько пользователей (сервер, другие клиенты) — немедленно переходить к однозначному представлению в хранилище (т.е. UTC) и к конвертации в локальное время на клиентах (они свои часовые пояса знают лучше, чем сервер).
При расчете будущих событий UTC не поможет, т.к. события, обычно, должны происходить в какое-то точное локальное время.
Скажем, как бы вы решали такую проблему наименьшими усилиями: надо расчитать даты запуска джоба в 10 утра по каждой из нескольких локаций, которые находятся в разных часовых поясах?
По моим расчетам, можно и через UTC: расчитываем последовательность, переводим в локальную, чтобы проверить 10 часов это или нет, сохраняем обратно в UTC.
А пожно просто забить как 10 утра локально и скедулер уже будет вызывать, без конвертаций и расчетов.

В чем видите тут проблему?
Задание, я так понимаю, выполняется на лондонском сервере, и должно отработать в 10 часов владивостокского времени, даже если Владик сейчас оффлайн и результатов не получит? Потому что если Владик онлайн, то у него может работать свой планировщик, который ровно в 10 местного времени будет делать запрос в Лондон и инициировать задачу на центральном сервере. Если Владик оффлайн, то зачем вообще нагружать сервер задачей, результатов которой тот в ближайшее время, возможно, не увидит?

Но, в принципе, если у вас ограниченное число хорошо известных локаций и нет проблем обновлять таймзоны по ним, то да, ваше решение тоже работает. Мои замечания больше относятся к системам, которая должны обслуживать неопределённое число клиентов в любой мыслимой и немыслимой зоне.
Как определять таймзону не является большой проблемой. Проблема — сохранение этой информации в цикле разных расчетов и чтений-записи данных. Если эта информация всегда будет с date/time, то проблем почти не будет.
Только DateTimeOffset! Без вариантов. Никаких UTC, — это костыль.

Нужно хранить именно DateTimeOffset, т.к. при приведении к UTC теряется информация, какое локальное время и смещение были в момент регистрации события.


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


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


И также верно выше заметили, что если совсем по-хорошему, то кроме смещения нужно хранить и название (или код) тайм-зоны.

UFO just landed and posted this here
Sign up to leave a comment.

Articles