Перейти к содержимому
Войти Регистрация

Топ 6 ошибок при работе с датами и временем

Топ 6 ошибок при работе с датами и временем

Даты кажутся простой темой ровно до первого отчёта, который разошёлся на один день, или уведомления, пришедшего в три часа ночи. Шесть ошибок, которые допускают почти все, — и способы их не допускать.

Почему время такое сложное

Потому что за словом «время» прячутся три разные вещи. Момент на оси — конкретная точка, одинаковая для всех людей на планете. Календарное обозначение — «пятнадцатое марта, девять утра», которое без указания места ничего не значит. И длительность — «через два дня», которая, как ни странно, не всегда равна сорока восьми часам.

Большинство ошибок начинается там, где эти три вещи перепутали. Момент пытаются хранить как календарную запись, календарную запись — сравнивать с моментом, а к моменту прибавлять дни так, будто в сутках всегда одинаковое число секунд.

Вторая причина. Правила времени задаются не физикой, а решениями властей. Границы поясов, смещения и переходы меняются, объявляются иногда за считаные недели и распространяются обновлением базы часовых поясов. Никакая формула это не заменяет — нужна актуальная база данных.

И третья. На машине разработчика всё работает. Один часовой пояс, одна локаль, тесты гоняются днём в будни. Проблемы начинаются, когда появляются пользователи из другого пояса, сервер в третьем, а база настроена в четвёртом.

Правила времени задаются не физикой, а решениями властей — и меняются без предупреждения.
Правила времени задаются не физикой, а решениями властей — и меняются без предупреждения. Фото: klynslis · BY.

Топ 6 ошибок

Хранить местное время вместо момента в UTC

самая дорогая ошибка: она не проявляется, пока не станет поздно

В базе лежит «15.03.2026 09:00», и никто не знает, чьи это девять утра. Пока проект живёт в одном городе, всё сходится. Как только появляется пользователь из другого пояса или сервер переезжает — данные становятся неоднозначными, и восстановить смысл задним числом уже нельзя.

Правило. Момент времени хранится в UTC, а в местное переводится только на границе — при показе пользователю. Все вычисления, сравнения и сортировки — в UTC. В базе для этого есть тип, который хранит момент с учётом зоны; выбирайте его, а не «время без зоны».

Что перевести в UTC заодно. Часы серверов, контейнеров и заданий по расписанию. Тогда логи разных машин складываются в одну картину, и не придётся в момент разбора аварии вычитать смещения в уме.

Важное исключение. Есть значения, у которых момента нет вовсе: дата рождения, дата документа, «магазин открывается в девять». Их не нужно переводить в UTC — это календарные значения, и хранить их следует отдельным типом даты или времени без зоны. Перевод дня рождения в UTC — классический способ сдвинуть его на день у половины пользователей.

  • Данные однозначны независимо от того, где стоит сервер
  • Сравнение и сортировка работают правильно всегда
  • Логи разных машин наконец складываются в одну ленту
  • Смотреть в базу глазами становится менее удобно
  • Перевод уже накопленных данных — отдельная непростая миграция
  • Правило легко нарушить в одном забытом месте кода

критичная ошибка · оценка 9,5

Путать смещение и часовой пояс

выглядит как придирка, а ломает будущие события

Смещение — это «плюс три часа от UTC», число. Часовой пояс — это правило, по которому вычисляется смещение в конкретный момент: у него есть история изменений и планы на будущее. Одно не заменяет другое.

Где это стреляет. На будущих событиях. Вы сохранили встречу «через три месяца в девять утра» вместе со смещением. За это время в регионе поменялись правила — и встреча уезжает на час, потому что смещение вы зафиксировали, а правило изменилось. Для будущих событий, привязанных к календарю человека, хранить нужно название зоны, а не число.

Правильное название. Зоны в общепринятой базе называются по городу и региону — вроде «Европа, Москва». Именно такие идентификаторы стоит хранить и передавать. Трёхбуквенные сокращения из писем и интерфейсов неоднозначны: одна и та же аббревиатура в разных странах означает разное.

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

  • Будущие события не разъезжаются при смене правил
  • Названные зоны читаемы и однозначны
  • Обновления правил приезжают вместе с обновлением базы зон
  • Нужно следить за свежестью базы часовых поясов
  • Пользователи не понимают, зачем у них спрашивают город

частая ошибка · оценка 9,3

Наивная арифметика с месяцами и годами

31 января плюс один месяц — и внезапно надо принимать решение

Прибавление месяца не определено математически. Что такое «31 января плюс месяц»? Разные библиотеки отвечают по-разному: 28 февраля, 3 марта или ошибка. Ни один ответ не является правильным сам по себе — правильный тот, который соответствует вашему бизнес-правилу, и это правило должно быть записано явно.

Високосный год. Подписка, оформленная 29 февраля, продлевается 28-го или 1 марта? Пользователь с днём рождения 29 февраля — когда его поздравлять? Пока правило не выбрано осознанно, за вас его выберет библиотека, и обычно не так, как ожидает бухгалтерия.

Границы месяца и недели. «Отчёт за март» — это интервал от полуночи первого марта до полуночи первого апреля, не включая правую границу. Полуоткрытые интервалы избавляют от вечных вопросов про последнюю секунду и про то, попадёт ли в отчёт запись, созданная в 23:59:59,7. Считать «конец месяца» как 23:59:59 — источник тихо теряющихся записей.

И про сутки. Прибавить сутки и прибавить 24 часа — не одно и то же в регионах с переводом часов. Если вам нужно «то же время завтра», работайте с календарными датами в зоне пользователя, а не с секундами.

  • Осознанно выбранное правило снимает споры навсегда
  • Полуоткрытые интервалы упрощают все запросы к базе
  • Проверяется тестами на конкретных датах
  • Правило приходится согласовывать с заказчиком, а не выбирать самому
  • Разные библиотеки ведут себя по-разному, и переносить код больно

частая ошибка · оценка 9,2

Забывать про переход на летнее время

два раза в год ломает расписания у половины планеты

В регионах с переводом часов есть сутки, где 23 часа, и сутки, где 25. При переходе вперёд один час не существует вовсе — попытка создать событие на 02:30 в такую ночь даёт либо ошибку, либо тихий сдвиг. При переходе назад час повторяется дважды, и момент «01:30» становится неоднозначным: их два, и они отличаются на час.

Что ломается на практике. Задания по расписанию: ночная задача выполняется дважды или пропускается. Отчёты по часам: в одном столбце внезапно два часа данных. Расчёт длительности между двумя местными временами: смена в двенадцать часов оказывается одиннадцати- или тринадцатичасовой, и это не ошибка, а правда.

В России перевода часов сейчас нет — но это ровно та причина, по которой наши разработчики про него забывают. Достаточно одного пользователя, партнёра или сервера в стране, где перевод есть, и проблема ваша.

Как защищаться. Регулярные задачи планировать в UTC, если для них важна периодичность, и в местном времени, если важно «каждый день в девять утра по-местному» — но тогда честно обрабатывать несуществующий и удвоенный час. Длительности считать по моментам в UTC, а не вычитанием местных времён.

  • После правильной настройки тема закрывается надолго
  • Готовые библиотеки планирования умеют это учитывать
  • Легко воспроизвести в тестах — даты переходов известны заранее
  • Проявляется два раза в год, а отлаживается по горячим следам
  • Часть решений противоречива, и выбирать приходится вручную

частая ошибка · оценка 9,1

Сравнивать и сортировать даты как строки

работает ровно до первого другого формата

Строковое сравнение дат работает в одном-единственном случае: формат «год-месяц-день» с ведущими нулями, одинаковый у всех значений. Тогда лексикографический порядок совпадает с хронологическим. Во всех остальных случаях получается мусор: «03.05.2026» и «15.02.2026» сортируются по первой цифре, а «9» оказывается больше «10».

Где это заводится. В экспортах и импортах, в идентификаторах файлов, в ключах кеша, в фильтрах отчётов, куда даты приезжают из формы строкой и так и уходят в запрос. Особенно коварно, когда часть данных в одном формате, а часть — в другом: сортировка выглядит почти правильной, и ошибку замечают через месяцы.

Как правильно. Сравнивать значениями подходящего типа, а не текстом. Если по каким-то причинам дата всё же живёт строкой — только в стандартном формате ISO 8601 с ведущими нулями и с явным указанием зоны или пометкой UTC.

Смежная беда — сравнение моментов разной точности. Значение с миллисекундами не равно тому же значению без них, и проверка на равенство внезапно перестаёт срабатывать. Отсюда пропадающие записи на границах интервалов: сравнивайте диапазонами, а не точным совпадением.

  • Исправляется быстро и без изменения логики
  • Стандартный формат читаем и людьми, и машинами
  • Правильные типы ловят проблему ещё на этапе компиляции или проверки схемы
  • В унаследованных выгрузках форматы уже намешаны, и чинить приходится данные
  • Строковые ключи с датами удобны, и от них жалко отказываться

частая ошибка · оценка 9,0

Разбирать локализованные форматы на глаз

«01/02/2026» — это первое февраля или второе января?

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

Названия месяцев и дней. Разбор «15 марта» или сокращений зависит от локали, установленной в системе. Один и тот же код на машине разработчика и на сервере с другими настройками ведёт себя по-разному — классическая история «локально работает, на сервере падает».

Как правильно. На границе системы принимать даты в машинном формате: стандартный ISO 8601 или отдельные числовые поля. Разбор с явно указанным шаблоном и явно указанной локалью, без опоры на настройки окружения. И проверка результата: если разбор не удался — ошибка, а не «подставим сегодняшнюю дату».

Отдельно про показ. Пользователю, наоборот, дату нужно показывать в его формате и на его языке — и здесь как раз уместны средства локализации. Правило простое: машинный формат внутри, человеческий — только на выходе.

  • Стандартный формат на входе убирает неоднозначность полностью
  • Явная локаль делает поведение одинаковым везде
  • Проверка результата разбора ловит мусор сразу
  • Не всегда можно диктовать формат: чужие выгрузки приходят как есть
  • Пользователям неудобно вводить даты в машинном виде — нужен отдельный слой преобразования

частая ошибка · оценка 8,7

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

Сравнение по главному

ОшибкаКак частоЦенаИсправить
Местное время вместо UTCчастокритичнаямиграция
Смещение вместо зонычастовысокаячасы
Арифметика с месяцамичастосредняячасы
Переход на летнее времяиногдавысокаячасы
Сравнение строкамичастосредняяминуты
Разбор локализованных форматовиногдасредняяминуты
Диаграмма важности шести ошибок при работе с датами и временем
Оценка — насколько дорого ошибка обходится и насколько часто встречается на практике.

Что настроить один раз и забыть

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

Одна библиотека на весь проект. Смешивать встроенные средства языка со сторонней библиотекой — верный способ получить два разных поведения в соседних файлах. Выберите одну и опишите правило в документации проекта.

Текущее время — через одну точку. Не обращайтесь к системным часам напрямую по всему коду: заведите способ получить «сейчас», который можно подменить. Без этого тесты на границы месяцев и переходы времени написать невозможно, а с ним — тривиально.

Тесты на конкретные даты. 29 февраля, 31 января, ночь перехода на летнее время, полночь на границе месяца, пользователь на другом конце мира. Пять таких тестов ловят почти всё описанное выше. Про то, какие тесты вообще стоит писать, у нас есть отдельный материал.

Свежая база часовых поясов. Она обновляется вместе с системой или библиотекой. На долгоживущем сервере, который никто не обновляет, правила застревают в прошлом — и это тихо ломает будущие события.

Частые вопросы

Хранить момент или дату с зоной?

Зависит от смысла. Для того, что уже произошло, — момент в UTC: он однозначен и не изменится. Для будущих событий, привязанных к календарю человека («созвон 3 июня в 10 утра по Москве»), хранить нужно местное время плюс название зоны, иначе изменение правил сдвинет встречу. На практике часто хранят и то и другое: момент для сортировки, исходное намерение — для пересчёта.

Нужна ли отдельная библиотека для дат?

В современных языках встроенных средств обычно достаточно, и лишняя зависимость не нужна. Смотреть в сторону библиотеки стоит, если встроенный набор в вашем языке исторически неудачный или вам нужны сложные календарные правила и разбор нестандартных форматов. Главное — не тащить сразу две.

Как тестировать переходы на летнее время?

Подменять текущее время и явно указывать зону в тесте — например, любую зону с переводом часов — и проверять поведение на конкретную дату перехода. Такие даты известны заранее и лежат в базе часовых поясов. Проверять стоит три вещи: несуществующий час, удвоенный час и длительность интервала, пересекающего переход.

Что делать со временем, пришедшим от клиента?

Не доверять ему как источнику истины. Часы на устройстве могут отставать, спешить или быть выставлены вручную. Момент события фиксируйте на сервере, а клиентское время принимайте только там, где оно и есть смысл, — например, когда пользователь сам указывает дату. И всегда проверяйте на разумность: дата из прошлого века или из будущего года — почти наверняка ошибка ввода.

Итог

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

Второе по важности — завести подменяемое «сейчас» и написать пять тестов на неудобные даты. Это работа на один вечер, после которой ошибки с датами перестают приходить из прода в виде вопросов «почему отчёт разошёлся на день».

0
Оценили 0 читателей

Комментарии

0
Г
Без регистрации можно оставить один комментарий к публикации. Войдите, чтобы участвовать в обсуждении дальше и получать ответы. Ссылки в комментариях скрываются.
Пока нет комментариев

Будьте первым, кто ответит автору.