Почти во всех проектах возникают проблемы, вызванные неправильной обработкой и хранением даты и времени.
Даже если проект используется в том же часовом поясе, при переходе на зимнее/летнее время все равно можно получить неприятные сюрпризы.
При этом мало кто озадачивается реализацией правильного механизма с самого начала, ведь кажется, что с этим проблем быть не может, поскольку все тривиально.
К сожалению, более поздняя реальность показывает, что это не так.
Логично можно выделить следующие типы значений, связанных с датой и временем:
- Дата и время — «материал для медицинского анализа собран 15 января 2014 года в 13:17:15»
- Дата без времени - например, «новый договор вступает в силу 2 февраля 2016 года»
- Временной интервал — «отчет был сформирован за 3 минуты 15 секунд»
- График запланированных мероприятий — «импорт данных из другой системы должен происходить каждый будний день в 10:00»
Дата и время
Допустим, лаборатория, собравшая материал на анализ, находится в часовом поясе +2, а центральный филиал, контролирующий своевременное выполнение анализов, – в поясе +1. Время, указанное в примере, было отмечено, когда материал был собран первой лабораторией.Возникает вопрос – какую цифру времени должен видеть центральный офис? Очевидно, ПО центрального офиса должно показывать 15.01.2014 12:17:15 - на час меньше, так как по их часам событие произошло именно в этот момент. Рассмотрим одну из возможных цепочек действий, посредством которых данные проходят от клиента к серверу и обратно, что позволяет всегда корректно отображать дату/время согласно текущему часовому поясу клиента:
- Значение создается на клиенте, например 2 марта 2016 г.
15 :13:36, клиент находится в часовом поясе +2.
- Значение преобразуется в строковое представление для передачи на сервер — «2016-03-02T 15 :13:36+02:00”.
- Сериализованные данные отправляются на сервер.
- Сервер десериализует время в объект даты/времени, приводя его к текущему часовому поясу.
Например, если сервер работает с номером +1, то объект будет содержать дату 2 марта 2016 г.
14 :13:36.
- Сервер сохраняет данные в базу данных, но никакой информации о часовом поясе она не содержит — наиболее часто используемые типы даты/времени просто ничего о нем не знают. Таким образом, 2 марта 2016 года будет сохранено в базе данных.
14 :13:36 в «неизвестном» часовом поясе.
- Сервер считывает данные из базы данных и создает соответствующий объект со значением 2 марта 2016 г.
14 :13:36. А поскольку сервер работает в часовом поясе +1, это значение также будет интерпретироваться в пределах того же часового пояса.
- Значение преобразуется в строковое представление для передачи клиенту — «2016-03-02T 14 :13:36+01:00”.
- Сериализованные данные отправляются клиенту.
- Клиент десериализует полученное значение в объект даты/времени, приводя его к текущему часовому поясу.
Например, если это -5, то отображаемое значение должно быть 2 марта 2016 г.
09 :13:36.
На самом деле проблемы здесь могут случиться практически на каждом этапе.
- Время на клиенте может генерироваться вообще без часового пояса — например, тип DateTime в .
NET с DateTimeKind.Unspecified.
- Механизм сериализации может использовать формат, который не включает смещение часового пояса.
- При десериализации в объект смещение часового пояса можно игнорировать, особенно в «самодельных» десериализаторах — как на сервере, так и на клиенте.
- При чтении из базы данных объект даты/времени может быть сгенерирован вообще без часового пояса — например, тип DateTime в .
NET с DateTimeKind.Unspecified. Более того, с DateTime в .
NET на практике именно так и происходит, если сразу после корректуры не указать явно другой DateTimeKind.
- Если серверы приложений, работающие с общей базой данных, будут находиться в разных часовых поясах, возникнет серьезная путаница в смещениях времени.
Значение даты/времени, записанное в базу данных сервером A и прочитанное сервером B, будет заметно отличаться от того же исходного значения, записанного сервером B и прочитанного сервером A.
- Перенос серверов приложений из одной зоны в другую приведет к неправильной интерпретации уже сохраненных значений даты/времени.
Если не будет перехода на летнее/зимнее время, то и дополнительных проблем не возникнет. Но в противном случае можно получить массу неприятных сюрпризов.
Правила перевода на летнее/зимнее время, строго говоря, различны.
Разные страны могут время от времени менять свои правила, и эти изменения следует заблаговременно закладывать в обновления системы.
На практике неоднократно встречались ситуации некорректной работы этого механизма, которые в конечном итоге решались установкой исправлений либо для операционной системы, либо для используемых сторонних библиотек.
Вероятность повторения одних и тех же проблем не равна нулю, поэтому лучше иметь способ избежать их.
Принимая во внимание изложенные выше соображения, сформулируем наиболее надежный и простой подход к передаче и хранению времени: на сервере и в базе данных все значения должны быть преобразованы в часовой пояс UTC .
Давайте посмотрим, что нам дает это правило:
- При отправке данных на сервер клиент должен передать смещение часового пояса, чтобы сервер мог правильно преобразовать время в формат UTC. Альтернативный вариант — заставить клиента выполнить это преобразование, но первый вариант более гибкий.
При получении данных обратно с сервера клиент преобразует дату и время в свой местный часовой пояс, зная, что в любом случае он получит время в формате UTC.
- Переходов между летним и зимним временем в UTC нет, поэтому проблемы связанные с этим не будут актуальны.
- На сервере при чтении из базы данных конвертировать значения времени не нужно; вам просто нужно явно указать, что оно соответствует UTC. Например, в .
NET этого можно добиться, установив для DateTimeKind объекта времени значение DateTimeKind.Utc.
- Разница часовых поясов между серверами, работающими с общей базой данных, а также перенос серверов из одной зоны в другую никоим образом не повлияют на корректность полученных данных.
- Сделайте механизм сериализации и десериализации таким, чтобы значения даты/времени корректно переводились из UTC в местный часовой пояс и обратно.
- Убедитесь, что десериализатор на стороне сервера создает объекты даты и времени в формате UTC.
- Убедитесь, что при чтении из базы данных объекты даты/времени создаются в формате UTC. Этот пункт иногда предоставляется без изменений кода — просто системный часовой пояс на всех серверах выставлен на UTC.
- Системные требования не требуют, чтобы местное время и/или смещение часового пояса отображались точно в том виде, в котором они были сохранены.
Например, на авиабилетах должно быть указано время вылета и прибытия в часовом поясе, соответствующем местоположению аэропорта.
Или, если сервер отправляет на печать счета, созданные в разных странах, в каждом из них должно быть указано местное время, а не преобразованное в часовой пояс сервера.
- Все значения даты и времени в системе являются «абсолютными» — т.е.
описывают момент времени в будущем или прошлом, который соответствует одному значению в формате UTC. Например, «запуск ракеты-носителя состоялся в 23:00 по киевскому времени» или «встреча состоится с 13:30 до 14:30 по минскому времени».
Числа этих событий будут разными в разных часовых поясах, но они будут описывать один и тот же момент времени.
Но возможно, что в некоторых случаях требования к программному обеспечению подразумевают «относительное» местное время.
Например, «эта телепрограмма будет выходить в эфир с 9:00 до 10:00 во всех странах, где есть филиалы телеканала».
Получается, что выход в эфир программы — это не одно событие, а несколько, и потенциально все они могут происходить в разные промежутки времени в «абсолютном» масштабе.
Ниже приведен небольшой список примеров для разных платформ и СУБД.
.
СЕТЬ |
ДатаВремяСмещение |
Джава | org.joda.time.DateTime, java.time.ZonedDateTime |
MS SQL | смещение даты и времени |
Oracle, PostgreSQL | ВРЕМЯ С ЧАСОВЫМ ПОЯСОМ |
MySQL | — |
Если это «относительное» время необходимо сохранить просто для отображения, и нет задачи определить «абсолютный» момент времени, когда событие произошло или произойдет для данного часового пояса, достаточно просто отключить преобразование времени.
Например, пользователь ввел запуск программы для всех филиалов телекомпании 25 марта 2016 года в 9:00, и в таком виде она будет передаваться, сохраняться и отображаться.
Но может случиться так, что какой-нибудь планировщик должен за час до начала каждой передачи автоматически выполнять специальные действия (рассылать уведомления или проверять наличие каких-то данных в базе телекомпании).
Надежная реализация такого планировщика — нетривиальная задача.
Допустим, планировщик знает, в каком часовом поясе находится каждая ветка.
И одна из стран, где есть ветка, через какое-то время решает сменить часовой пояс.
Случай не такой редкий, как может показаться - за этот и за два предыдущих года я насчитал более 10 таких событий( http://www.timeanddate.com/news/time/ ).
Получается, что либо пользователи должны поддерживать привязки часовых поясов в актуальном состоянии, либо планировщик должен автоматически получать эту информацию из глобальных источников, таких как API часовых поясов Google Maps. Я не берусь предлагать универсальное решение для таких случаев, просто отмечу, что такие ситуации требуют серьёзного изучения.
Как видно из вышеизложенного, не существует единого подхода, охватывающего 100% случаев .
Поэтому вам сначала нужно из требований четко понять, какая из вышеперечисленных ситуаций произойдет в вашей системе.
Скорее всего, всё ограничится первым предложенным подходом с хранением в UTC. Ну а описанные исключительные ситуации его не отменяют, а просто добавляют другие решения для особых случаев.
Дата без времени
Допустим, мы разобрались с корректным отображением даты и времени с учетом часового пояса клиента.Перейдем к датам без времени и примеру, приведенному для этого случая в начале – «новый договор вступает в силу 2 февраля 2016 года».
Что произойдет, если для таких значений использовать те же типы и тот же механизм, что и для «обычных» дат и времени? Не все платформы, языки и СУБД имеют типы, предназначенные только для дат. Например, в .
NET есть только тип DateTime, отдельного типа «просто Дата» нет. Даже если при создании такого объекта была указана только дата, время все равно присутствует и равно 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 .
Он охватывает практически все потребности в планировании, включая такие варианты, как «вторая пятница месяца».
В большинстве случаев писать собственный планировщик не имеет смысла, так как есть гибкие, проверенные временем решения, но если по каким-то причинам возникает необходимость создать собственный механизм, то как минимум формат расписания можно позаимствовать.
из крона.
Общие рекомендации
Помимо описанных выше рекомендаций по хранению и обработке разных типов значений времени, есть еще несколько, о которых хотелось бы упомянуть.Во-первых, что касается использования статических членов класса для получения текущего времени — DateTime.UtcNow, ZonedDateTime.now() и т.д. Как было сказано, использование их непосредственно в коде может серьезно усложнить юнит-тестирование, так как без специальных мокинг-фреймворков его не получится.
можно заменить текущее время.
Поэтому, если вы планируете писать модульные тесты, вам следует убедиться, что реализацию таких методов можно заменить.
Есть как минимум два пути решения этой проблемы:
- Предоставьте интерфейс IDateTimeProvider с одним методом, который возвращает текущее время.
Затем добавьте зависимость от этого интерфейса во все блоки кода, где вам нужно получить текущее время.
При обычном выполнении программы во все эти места будет внедрена реализация «по умолчанию», которая возвращает реальное текущее время, а в модульных тестах — любая другая необходимая реализация.
Этот метод является наиболее гибким с точки зрения тестирования.
- Создайте свой статический класс с методом получения текущего времени и возможностью установки любой реализации этого метода извне.
Например, в случае кода C# этот класс может предоставлять свойство UtcNow и SetImplementation(Func реализовать) метод. Использование статического свойства или метода для получения текущего времени избавляет от необходимости везде явно указывать зависимость от дополнительного интерфейса, но с точки зрения принципов ООП это не идеальное решение.
Однако если по каким-то причинам предыдущий вариант не подходит, то можно воспользоваться и этим.
Эту проблему легко решить в большинстве систем контроля качества кода.
По сути, все сводится к поиску «нежелательной» подстроки во всех файлах, кроме того, где объявлена «дефолтная» реализация.
Второе предостережение относительно получения текущего времени заключается в том, что клиенту нельзя доверять .
Текущее время на компьютерах пользователей может сильно отличаться от реального, и если к этому привязана логика, то эта разница может все испортить.
Все места, где есть необходимость получения текущего времени, по возможности должны быть сделаны на стороне сервера.
И, как говорилось ранее, все арифметические операции со временем необходимо выполнять либо в значениях UTC, либо с использованием типов, хранящих смещение часового пояса.
И еще одна вещь, которую я хотел упомянуть, это стандарт ИСО 8601 , который описывает формат даты и времени для обмена информацией.
В частности, строковое представление даты и времени, используемое при сериализации, должно соответствовать этому стандарту, чтобы предотвратить потенциальные проблемы совместимости.
На практике крайне редко приходится реализовывать форматирование самостоятельно, поэтому сам стандарт может быть полезен в основном в информационных целях.
Теги: #данные #время #разработка сайтов #программирование
-
Стражи Запредельного Мира: Ведьмвилль
19 Oct, 24 -
Как Мы Поймали Ux-Дизайнера На Живца
19 Oct, 24 -
О Кастах
19 Oct, 24 -
Дубликаты В Rss-Ленте.
19 Oct, 24 -
Как Работает Sip-Клиент В Браузере
19 Oct, 24 -
Пишем Бота Для Мессенджера Tox
19 Oct, 24