Логически можно выделить следующие типы значений, относящиеся к дате и времени:
- Дата и время — «материал для медицинского анализа был собран 15 января 2014 года в 13:17:15»
- Дата без времени — например, «новый контракт вступает в силу 2 февраля 2016 года»
- Временной интервал — «отчет был сформирован за 3 минуты 15 секунд»
- Расписание запланированных событий — «импорт данных из другой системы должен происходить каждый будний день в 10:00»
Рассмотрим каждый пункт по отдельности, не забывая об общих рекомендациях.
Дата и время
Допустим, лаборатория, которая собрала материал для анализа находится в часовом поясе +2, а центральный филиал, в котором следят за своевременным выполнением анализов — в поясе +1. Время, приведенное в примере, было отмечено при сборе материала первой лабораторией. Возникает вопрос — какую цифру времени должен увидеть центральный офис? Очевидно, что программное обеспечение центрального офиса должно показывать 15 января 2014 года 12:17:15 — на час меньше, так как по их часам событие произошло именно в этот момент.
Рассмотрим одну из возможных цепочек действий, через которую проходят данные с клиента на сервер и обратно, позволяющую всегда корректно отображать дату/время согласно текущему часовому поясу клиента:
- Значение создается на клиенте, например, 2 марта 2016 15:13:36, клиент находится в часовом поясе +2.
- Значение преобразуется в строковое представление для передачи на сервер — “2016-03-02T15:13:36+02:00”.
- Сериализованные данные передаются на сервер.
- Сервер десериализует время в объект даты/времени, приводя его при этом к своему текущему часовому поясу. Например, если сервер работает в +1, значит объект будет содержать 2 марта 2016 14:13:36.
- Сервер сохраняет данные в базу, при этом она не содержит никакой информации о часовом поясе — наиболее часто используемые типы даты/времени просто о нем ничего не знают. Таким образом, в базу будет сохранено 2 марта 2016 14:13:36 в «неизвестном» часовом поясе.
- Сервер читает данные из базы и создает соответствующий объект со значением 2 марта 2016 14:13:36. И так как сервер работает в часовом поясе +1, то и это значение будет трактоваться в рамках того же часового пояса.
- Значение преобразуется в строковое представление для передачи на клиент — “2016-03-02T14:13:36+01:00”.
- Сериализованные данные отправляются на клиент.
- Клиент десериализует полученное значение в объект даты/времени, приводя его при этом к своему текущему часовому поясу. Например, если это -5, то отображаемое значение должно быть равно 2 марта 2016 09:13:36.
Вроде бы все целостно, но давайте подумаем, что в этом процессе может пойти не так. На самом деле, проблемы здесь могут случиться почти на каждом шаге.
- Время на клиенте может быть сформировано вообще без часового пояса — например, DateTime тип в .NET с DateTimeKind.Unspecified.
- Механизм сериализации может использовать формат, который не включает в себя смещение часового пояса.
- При десериализации в объект может игнорироваться смещение часового пояса, особенно в «самодельных» десериализаторах — как на сервере, так и на клиенте.
- При чтении из базы объект даты/времени может быть сформирован вообще без часового пояса — например, DateTime тип в .NET с DateTimeKind.Unspecified. Более того, с DateTime в .NET на практике именно так и получается, если сразу после вычитки явно не указывать другой DateTimeKind.
- Если сервера приложения, работающие с общей базой данных, находятся в разных часовых поясах — будет серьезная путаница в смещениях во времени. Значение даты/времени, записанное в базу сервером А и прочитанное сервером Б будет заметно отличаться от того же исходного значения, записанного сервером Б и прочитанного сервером А.
- Перевод серверов приложения из одного пояса в другой приведет к неправильной трактовке уже сохраненных значений даты/времени.
Но самый серьезный недостаток в описанной выше цепочке — это использование локального часового пояса на сервере. Если в нем нет перехода на летнее/зимнее время, то никаких дополнительных проблем не будет. А вот в противном случае можно получить массу неприятных неожиданностей.
Правила перевода на летнее/зимнее время — вещь, строго говоря, переменная. Разные страны могут иногда менять свои правила, и эти изменения должны быть заблаговременно заложены в обновления системы. На практике неоднократно встречались ситуации некорректной работы этого механизма, которые по итогу решались установкой хотфиксов либо операционной системы, либо используемых сторонних библиотек. Вероятность повторения тех же проблем — не нулевая, поэтому лучше иметь способ гарантированно их избежать.
Учитывая описанные выше соображения, сформулируем как можно более надежный и простой подход к передаче и хранению времени: на сервере и в базе данных все значения должны быть приведены к часовому поясу UTC.
Рассмотрим, что нам дает такое правило:
- При отправке данных на сервер клиент должен передать смещение часового пояса, чтобы сервер мог корректно преобразовать время в UTC. Альтернативный вариант — обязать клиент делать это преобразование, но первый вариант гибче. При получении данных обратно с сервера клиент будет переводить дату и и время в свой локальный часовой пояс зная, что ему в любом случае придет время в UTC.
- В UTC нет переходов на летнее и зимнее время, соответственно связанные с этим проблемы будут неактуальны.
- На сервере при чтении из базы не нужно преобразовывать значения времени, достаточно только явно указывать, что оно соответствует UTC. В .NET, например, этого можно достичь установкой DateTimeKind для объекта времени в DateTimeKind.Utc.
- Разница часовых поясов между серверами, работающими с общей базой данных, как и перевод серверов из одного пояса в другой, никак не отразятся на корректности получаемых данных.
Для реализации такого правила достаточно позаботиться о трех вещах:
- Сделать механизм сериализации и десериализации такой, чтобы значения даты/времени корректно переводились из UTC в локальный часовой пояс и обратно.
- Убедиться, что десериализатор на стороне сервера создает объекты даты/времени в UTC.
- Сделать так, чтобы при вычитке из базы объекты даты/времени создавались в UTC. Этот пункт иногда обеспечивают без изменений кода — просто системный часовой пояс на всех серверах устанавливается в UTC.
Описанные выше соображения и рекомендации отлично работают при сочетании двух условий:
- В требованиях к системе нет надобности отображать локальное время и/или смещение часового пояса именно в том виде, в котором оно было сохранено. Например, в авиабилетах время вылета и прибытия должно быть распечатано в часовом поясе, соответствующем расположению аэропорта. Или если сервер отправляет на печать инвойсы, созданные в разных странах, на каждом должно в итоге стоять локальное время, а не преобразованное в часовой пояс сервера.
- Все значения даты и времени в системе являются «абсолютными» — т.е. описывают момент времени в будущем или прошлом, которому соответствует единственное значение в UTC. Например, «пуск ракеты-носителя состоялся в 23:00 по киевскому времени», или «совещание будет идти с 13:30 до 14:30 по минскому времени». В разных часовых поясах цифры для этих событий будут разные, но они будут описывать один и тот же момент времени. Но может так случиться, что требования к программному обеспечению подразумевают «относительное» локальное время для некоторых случаев. Например, «эта передача на телевидении будет идти с 9:00 до 10:00 утра в каждой стране, где есть филиал телеканала». Получается, что показ передачи — это не одно событие, а несколько, причем потенциально все они могут происходить в разные отрезки времени по «абсолютной» шкале.
Для случаев, где нарушается первое условие, задачу можно решить использованием типов данных, содержащих часовой пояс – как на сервере, так и в базе данных. Ниже приведен небольшой перечень примеров для разных платформ и СУБД.
.NET | DateTimeOffset |
Java | org.joda.time.DateTime, java.time.ZonedDateTime |
MS SQL | datetimeoffset |
Oracle, PostgreSQL | TIMESTAMP WITH TIME ZONE |
MySQL | — |
Нарушение второго условия — более сложный случай. Если это «относительное» время нужно хранить просто для отображения, и нет задачи определить «абсолютный» момент времени, когда событие наступило или наступит для заданного часового пояса — достаточно просто запретить преобразование времени. Например, пользователь ввел начало передачи для всех филиалов телекомпании 25 марта 2016 года в 9:00, и оно в таком виде будет передаваться, храниться и отображаться. Но может случиться, что какой-то планировщик должен автоматически выполнить специальные действия за час до начала каждой передачи (разослать уведомления или проверить наличие каких-то данных в базе телекомпании). Надежная реализация такого планировщика является нетривиальной задачей. Допустим, планировщик осведомлен о том, в каком часовом поясе находится каждый из филиалов. И одна из стран, где есть филиал, через некоторое время решает сменить часовой пояс. Случай не настолько редкий, как может показаться — за этот и два предыдущих года я насчитал больше 10 подобных событий (http://www.timeanddate.com/news/time/). Получается, что либо пользователи должны поддерживать привязки к часовым поясам в актуальном состоянии, либо планировщик должен в автоматизированном режиме брать эту информацию из глобальных источников типа Google Maps Time Zone API. Я не берусь предложить универсальное решение для подобных случаев, просто отмечу, что такие ситуации требуют серьезной проработки.
Как видно из вышесказанного, не существует единого подхода, покрывающего 100% случаев. Поэтому сперва нужно четко понять из требований, какие из упомянутых выше ситуаций будут в вашей системе. С большой вероятностью, все ограничится первым предложенным подходом с хранением в UTC. Ну а описанные исключительные ситуации его не отменяют, а просто добавляют другие решения для частных случаев.
Дата без времени
Допустим, с правильным отображением даты и времени с учетом часового пояса клиента разобрались. Перейдем к датам без времени и примеру, указанному для этого случая в начале — «новый контракт вступает в силу 2 февраля 2016 года». Что будет, если для таких значений использовать те же типы и тот же механизм, что и для «обычных» даты с временем?
Не во всех платформах, языках и СУБД есть типы, хранящие только дату. Например, в .NET есть только тип DateTime, отдельного «просто Date» нет. Даже если при создании такого объекта была указана только дата, время все равно присутствует, и оно равно 00:00:00. Если мы значение «2 февраля 2016 00:00:00» из пояса со смещением +2 переведем в +1, то получим «1 февраля 2016 23:00:00». Для указанного выше примера это будет равносильно тому, что в одном часовом поясе новый контракт начнет действовать 2 февраля, а в другом — 1 февраля. С юридической точки зрения это абсурд и так, конечно же, быть не должно. Общее правило для «чисто» дат предельно простое — такие значения не должны преобразовываться ни на одном шаге сохранения и чтения.
Есть несколько способов избежать преобразование для дат:
- Если платформа поддерживает тип, представляющий дату без времени, то его и нужно использовать.
- Добавлять в метаданные объектов специальный признак, который будет говорить сериализатору, что для данного значения часовой пояс нужно игнорировать.
- Передавать дату с клиента и обратно как строку, а хранить как дату. Такой подход неудобен, если на клиенте дату нужно не только отображать, но еще и производить какие-то операции над ней: сравнение, вычитание и т.д.
- Передавать и хранить как строку, а преобразовывать в дату только для форматирования с учетом региональных настроек клиента. Имеет еще больше недостатков чем предыдущий вариант — например, если в хранимой строке части даты идут не в порядке «год, месяц, день», то будет невозможно сделать эффективный индексированный поиск по диапазону дат.
Можно, конечно, попытаться привести контрпример и сказать, что контракт имеет смысл только в пределах страны, в которой он заключен, страна находится в одном часовом поясе, и поэтому можно однозначно определить момент вступления его в силу. Но даже в этом случае пользователям из других часовых поясов не будет интересно, в какой момент по их локальному времени произойдет это событие. А даже если бы была нужда показывать этот момент времени, то отображать пришлось бы не только дату, но и время, что противоречит исходному условию.
Временной интервал
С хранением и обработкой временных интервалов все просто: их величина не зависит от часового пояса, поэтому никаких особых рекомендаций здесь нет. Их можно хранить и передавать как количество единиц времени (целое или с плавающей точкой, в зависимости от необходимой точности). Если важна секундная точность — то как количество секунд, если миллисекундная — то как количество миллисекунд и т.д.
А вот вычисление интервала может иметь подводные камни. Предположим, у нас есть типовой код на C#, который считает интервал времени между двумя событиями:
DateTime start = DateTime.Now;
//...
DateTime end = DateTime.Now;
double hours = (end - start).TotalHours;
На первый взгляд, никаких проблем здесь нет, но это не так. Во-первых, могут возникнуть проблемы с юнит-тестированием такого кода, но об этом мы поговорим чуть позже. Во-вторых, давайте представим, что начальный момент времени пришелся на зимнее время, а конечный — на летнее (например, таким образом замеряется количество рабочих часов, а у работников есть ночная смена).
Предположим, код работает в часовом поясе, в котором переход на летнее время в 2016 году происходит в ночь 27 марта, и смоделируем описанную выше ситуацию:
DateTime start = DateTime.Parse("2016-03-26T20:00:15+02");
DateTime end = DateTime.Parse("2016-03-27T05:00:15+03");
double hours = (end - start).TotalHours;
Этот код даст в результате 9 часов, хотя фактически между этими моментами прошло 8 часов. В этом легко убедиться, изменив код вот таким образом:
DateTime start = DateTime.Parse("2016-03-26T20:00:15+02").ToUniversalTime();
DateTime end = DateTime.Parse("2016-03-27T05:00:15+03").ToUniversalTime();
double hours = (end - start).TotalHours;
Отсюда вывод — любые арифметические операции с датой и временем нужно делать, используя либо UTC значения, либо типы, хранящие информацию о часовом поясе. А потом обратно переводить в локальные в случае надобности. С этой точки зрения, изначальный пример легко исправить, поменяв DateTime.Now на DateTime.UtcNow.
Этот нюанс не зависит от конкретной платформы или языка. Вот аналогичный код на Java, имеющий тот же недостаток:
LocalDateTime start = LocalDateTime.now();
//...
LocalDateTime end = LocalDateTime.now();
long hours = ChronoUnit.HOURS.between(start, end);
Исправляется он также легко — например, использованием ZonedDateTime вместо LocalDateTime.
Расписание запланированных событий
Расписание запланированных событий – более сложная ситуация. Универсального типа, позволяющего хранить расписания, в стандартных библиотеках нет. Но такая задача возникает не так уж редко, поэтому готовые решения можно найти без проблем. Хорошим примером является формат планировщика cron, который в том или ином виде используется другими решениями, например, Quartz: http://quartz-scheduler.org/api/2.2.0/org/quartz/CronExpression.html. Он покрывает практически все нужды составления расписаний, включая варианты типа «вторая пятница месяца».
В большинстве случаев писать свой планировщик не имеет смысла, так как существуют гибкие проверенные временем решения, но если по какой-то причине есть надобность в создании своего механизма, то как минимум формат расписания можно позаимствовать у cron.
Общие рекомендации
Помимо описанных выше рекомендаций, посвященных хранению и обработке разнотипных значений времени, есть еще несколько других, о которых тоже хотелось бы сказать.
Во-первых, по поводу использования статических членов класса для получения текущего времени — DateTime.UtcNow, ZonedDateTime.now() и т.д. Как и было сказано, использование их напрямую в коде может серьезно усложнить юнит-тестирование, так как без специальных мок фреймворков подменить текущее время не получится. Поэтому, если вы планируете писать юнит тесты, следует позаботиться о том, чтобы реализацию таких методов можно было подменить. Для решения этой задачи есть как минимум два способа:
- Выделить интерфейс IDateTimeProvider с единственным методом, возвращающим текущее время. Затем добавить зависимость на этот интерфейс во всех единицах кода, где нужно получать текущее время. При обычном выполнении программы во все эти места будет инжектиться реализация «по умолчанию», которая возвращает реальное текущее время, а в юнит тестах – любая другая необходимая реализация. Такой способ является наиболее гибким с точки зрения тестирования.
- Сделать свой статический класс с методом для получения текущего времени и возможностью установить любую реализацию этого метода извне. Например, в случае C# кода этот класс может выставлять наружу свойство UtcNow и метод SetImplementation(Func<DateTime> impl). Использование статического свойства или метода для получения текущего времени избавляет от надобности везде явно прописывать зависимость от дополнительного интерфейса, но с точки зрения принципов ООП не является идеальным решением. Однако, если по каким-то соображениям предыдущий вариант не подходит, то можно воспользоваться этим.
Дополнительная проблема, которую следует решить при переходе на свою реализацию провайдера текущего времени — это контроль за тем, чтобы никто «по старинке» не продолжил использовать стандартные классы. Эту задачу легко решить в большинстве систем контроля качества кода. По сути она сводится к поиску «нежелательной» подстроки во всех файлах за исключением того, где объявлена реализация «по умолчанию».
Второй нюанс с получением текущего времени — это то, что клиенту доверять нельзя. Текущее время на компьютерах пользователей может очень сильно отличаться от реального, и если есть логика, завязанная на него, то эта разница может все испортить. Все места, где есть необходимость получать текущее время, должны по возможности выполняться на стороне сервера. И, как уже было сказано ранее, все арифметические операции с временем должны производиться либо в UTC значениях, либо с использованием типов, хранящих смещение часового пояса.
И еще одна вещь, которую хотелось упомянуть — это стандарт ISO 8601, описывающий формат даты и времени для обмена информацией. В частности, строковое представление даты и времени, используемое при сериализации, должно соответствовать этому стандарту для предотвращения потенциальных проблем с совместимостью. На практике крайне редко приходится самому реализовывать форматирование, поэтому сам стандарт может быть полезен в основном в ознакомительных целях.
Комментарии (29)
YuriyIvon
09.03.2016 00:06Там далее в статье написано, что есть требования при которых такой подход не будет работать, и нужно хранить дополнительно смещение часового пояса. Противоречия нет, просто последовательные соображения )
lair
09.03.2016 00:08При общении с неизвестным клиентом эти требования есть всегда — поэтому проще и не формулировать правила "клиент должен все преобразовывать в UTC", оно все равно никогда не будет выполняться.
(и, кстати, описанной мной ситуации там нет)YuriyIvon
09.03.2016 00:12Если под неизвестным клиентом подразумевается, что серверное АПИ будет непонятно кто по итогу использовать, то соглашусь. Такое ограничение для "произвольных" клиентов может оказаться неочевидным и неожиданным. Чуть позже обновлю статью.
lair
09.03.2016 00:15+1Под неизвестным клиентом подразумевается любой, который вами полностью не контролируется. Даже если его код полностью вам подконтролен, но он установлен на неизвестном вам устройстве — это в данном контексте неизвестный вам клиент, потому что вы понятия не имеете, какой там часовой пояс.
YuriyIvon
09.03.2016 00:19Если код полностью подконтролен и нет требований, по которым есть надобность хранить часовой пояс клиента — не вижу проблемы сделать на клиенте преобразование в UTC перед отправкой. Тут абсолютно равноценно — что клиент преобразует сам, что перешлет на сервер локальное время + смещение, для того, чтобы сервер сам уже выполнил это преобразование.
lair
09.03.2016 00:23+1Тут абсолютно равноценно — что клиент преобразует сам, что перешлет на сервер локальное время + смещение, для того, чтобы сервер сам уже выполнил это преобразование.
Неравноценно. Вы потеряли информацию (например, что клиент думал, что у него часовой пояс +4, хотя по всем разумным признакам должен думать, что +3). Лучше всего это работает, если передавать одновременно часовой пояс (как название) и смещение, но это уже требует выхода за рамки стандартов, и потому редко когда реализуемо.
(и да, именно поэтому часовой пояс клиента надо хранить по умолчанию, а не только тогда, когда на это есть явные требования)YuriyIvon
09.03.2016 00:28Не совсем понял — эта информация о смещении для дальнейшего саппорта? Чтобы была возможность потом посмотреть, откуда всязалась ошибка в смещении? Если так — окей, понял.
lair
09.03.2016 00:32+1Для саппорта, для дополнительной хитрой логики на сервере, для принятия решений… если у вас есть информация, и ее хранение вам не доставляет (существенных) дополнительных усилий — не выбрасывайте ее. Вы не сможете получить ее обратно.
gleb_l
09.03.2016 00:15Второй нюанс с получением текущего времени — это то, что клиенту доверять нельзя
идеально, когда клиентское время вообще нигде не фигурирует в БД в качестве метки времени, а везде используется только время БД (для MS SQL — это getUTCDate()).
Но такое не всегда возможно сделать эффективно без лишнего roundtrip сервер-клиент-сервер. В таких случаях я в начале сессии клиент-сервер обычно вместе с каким-нибудь полезным ответом сервера (например, подтверждения авторизации) шлю клиенту UTC-время БД. Клиент в свою очередь, считает на своей стороне разницу между UTC-временем по своим часам и полученным от сервера, считает поправку, и использует эту поправку при отправке на сервер меток времени. Конечно, здесь появляется погрешность на время прохода пакета от клиента к серверу, но это все равно лучше, чем просто отправлять время на клиентской стороне. Такой подход мной использовался, например, в авторизационных токенах с временем жизни порядка минуты.
bobermaniac
09.03.2016 01:29Сыграем в смешную игру. Давно хотел в нее сыграть, а тут отличный повод выдался.
Менеджер Иванов, находясь в Москве, 2 марта 2016 года выдал поручение инженеру Петрову, находящемуся в командировке в Хабаровске. Сроком исполнения поручения в системе документооборота Ивановым было установлено 11 марта 2016 года без указания времени.
Вопрос — какой срок исполнения поручения должен быть отображен Петрову? При решении задачи учитывать, что:
1. система документооборота, используемая Ивановым и Петровым поддерживает установку времени для поручений;
2. рабочий день Иванова по нормативным документам — с 9 утра до 6 вечера, рабочий день Петрова не нормирован, график — 2/1, 11 марта у него выходной.HaruAtari
09.03.2016 01:42+5Такие вопросы надо выяснять у заказчика и фиксировать в документации, а не играть в подобные игры.
bobermaniac
09.03.2016 09:25Это я к вопросу о передачи дат без преобразований и часовых поясов. Довольно шаткое положение, как по мне.
YuriyIvon
09.03.2016 13:58Приведенный вами пример не является случаем даты без времени. Если менеджер и вводит только дату, то показываться требуемое время исполнения должно как дата + время, а это уже само по себе противоречит исходному условию. Более того — назначение срока исполнения без конечного времени само по себе выглядит не очень удачным решением. Если я выполню задание в 23:30 1 фераля по московскому времени или 3:30 ночи 2 февраля — может ли так случиться, что для бизнеса по итогу разницы между этими временами нет, хотя дата исполнения стоит 1 февраля?
YChebotaev
09.03.2016 01:45По московскому времени. В филиалах и командировках, как раз чтобы в такие игры не играть, ориентируются на время головной организации.
ov7a
09.03.2016 13:51+1А почему нельзя хранить метки в epoch time?
YuriyIvon
09.03.2016 13:59В смысле в количестве секунд, прошедших с 1 января 1970 года?
ov7a
09.03.2016 14:02Да
YuriyIvon
09.03.2016 14:22Это уже давно устаревший подход, он как минимум не позволяет:
- хранить смещение часового пояса
- хранить даты ранее 1970 года
- иметь точность выше секундной
- сразу визуально понимать при отладке, что за время записано в поле
ov7a
09.03.2016 14:27- если вам это очень надо для показа пользователю, храните отдельным полем. Преобразовать можно на стороне клиента
- отрицательные значения
- хранить ms epoch
- настройте отображение, если вам надо.
YuriyIvon
09.03.2016 14:28+2Зачем делать вручную все вышеперечисленное, если уже давно существуют типы, которые все это делают "под капотом"?
ov7a
09.03.2016 15:23Зачем писать свои велосипеды под таймзоны, когда
а) Работа с тамзонами — это боль и унижение. См, например, https://habrahabr.ru/company/mailru/blog/242645/ или https://habrahabr.ru/post/100741/ или http://stackoverflow.com/questions/178704/are-unix-timestamps-the-best-way-to-store-timestamps
б) что вы имеете под "вручную"? Конвертировать human-readable в timestamp и обратно? А ваша контвертация между конечным форматом и форматом для пользователя — это ли не велосипед с ручным приводом?
Кстати, укажите, где написано, что unix timestamp — устаревший подход, а ваш UTC — новый и хороший?michael_vostrikov
09.03.2016 15:51+3Извините, я не очень понял. Как хранение времени в формате unix timestamp избавляет от отображения этого времени по-разному для разных временных зон? И чем он отличается от того, что вы называете "ваш UTC"?
ov7a
09.03.2016 17:59Никак. "ваш UTC" — то, что описано в статье, хранение данных как UTC время.
michael_vostrikov
09.03.2016 20:17Я понял, что такое "ваш UTC". Я хотел узнать, чем ваше решение от него отличается. Начало отсчета unix time ведь находится в UTC, соответственно и сама метка тоже является временем в UTC.
YuriyIvon
09.03.2016 16:20Устаревший ваш подход только в том, что используется целочисленный тип, на замену которого уже давно есть типы "datetime", которые и отображают хорошо при чтении из базы и из дебага, а классы, которые есть в платформах еще и сами всю конверсию умеют делать с помощью утилитных методов.
Не "мой" метод — новый и хороший, это уже давно общая практика. Он по сути говорит о том же, что и вы — что нужно хранить в универсальном времени. Только вы говорите о том, что для этого нужно хранить какое-то там количество секунд, а я говорю о том, что для этого есть готовые типы, которые, более того, умеют еще и смещение таймзоны хранить, если надо. В чем проблема?zgmnkv
09.03.2016 17:45+1Внутри эти готовые типы (по крайней мере "Дата и время" из вашей классификации) скорее всего и хранят unix timestamp, или какую-то подобную метку времени. Просто во многих случаях гораздо удобнее мыслить timestamp-ами, так как они представляют собой просто абсолютный момент времени во вселенной, независимо от временной зоны, тогда не приходится говорить ни о каких конвертациях, вы просто сравниваете числа, передаете числа из одной зоны в другую, применяете ту или иную таймзону для отображения timestamp-а.
lair
В сочетании с "клиенту доверять нельзя" (а ему доверять нельзя) вы получаете потерю информации. Вы не знаете, какой часовой пояс был у клиента, и как он его преобразовал в UTC. Более того, если клиент передает время в UTC, не указывая, что это UTC (т.е., тогда, когда вы отказались от контроля за передачей смещения), вы не знаете, передал он UTC, или же свое локальное время.
Намного проще требовать от клиента время с явно указанным смещением — в этом случае вы не теряете никакой информации, но всегда гарантированно можете получить из этого UTC.
YuriyIvon
Сорри, промахнулся с ответом — он ниже в комментариях.