Временные зоны — это ложь (и как с ними работать в коде)
Временные зоны выглядят как простые смещения UTC. Они не такие. В этом руководстве объясняется, почему временные зоны нарушают код — пропуски в переходных периодах, смещения на полчаса, неоправданные даты — и как правильно обрабатывать их на JavaScript, Python, PHP и SQL.
Вы спросили у своего сервера, какое время, и он ответил 14:00. Вы сохранили это. Вы запросили его обратно. Теперь оно говорит 16:00. Вы ничего не изменили. Добро пожаловать в часовые пояса.
Часовые пояса — один из самых обманчиво сложных вопросов в программировании. На поверхности они выглядят как простое смещение по UTC — просто добавляйте или вычитайте несколько часов. На самом деле, это хаотичная смесь политических решений, исторических случайностей, смещений на полчаса и правил перехода на летнее время, которые меняются без предупреждения. В этой статье разбирается, почему часовые пояса так сложны, и, что еще важнее, как правильно обрабатывать их в коде.
Почему «Просто используйте UTC» — это только половина ответа
Наиболее распространённый совет, который вы услышите, звучит так: храните всё в UTC. Этот совет правильный — но он неполный. Хранение в UTC решает проблему (согласованное хранение), но оставляет большую проблему без решения: отображение и ввод. один проблема
Пользователь в Токио планирует встречу в 9 утра по его местному времени. Вы сохраняете это как UTC. Позже пользователь в Нью-Йорке открывает тот же событие. Вы отображаете его в его локальном времени. Но чьё локальное время применяется, когда пользователь путешествует? Когда он обновляет системный часы? Когда начинается переход на летнее время? «Храните UTC, отображайте локально» — это правильная политика — она является выполнением которое ломает большинство команд.
Фактические проблемы с часовыми поясами
Перед тем как перейти к решениям, полезно назвать то, что вы действительно боретесь.
1. UTC-смещения не являются часовыми поясами
UTC+5:30 не является «временем Индии». Это статическое смещение. Индийское стандартное время — это Asia/Kolkata — названный часовой пояс, который использует +5:30 и никогда не наблюдал переход на летнее время. Это разные вещи. Если вы фиксируете смещение, вы храните число. Если вы храните названный пояс, вы храните намерение.
Названные пояса существуют в Базе данных IANA Time Zone (также называемой tzdata или базой Olson). Каждая серьёзная язык и ОС включает копию этой базы. Используйте её. Всегда предпочитайте America/New_York как UTC-5.
2. Переходы на летнее время перемещают часы (непредсказуемо)
Переходы на летнее время создают два действительно опасных крайних случая:
- Случай «перехода весной»: Часы прыгают с 2:00 до 3:00. Время 2:30 фактически не существует. Если вы пытаетесь запланировать что-то в 2:30 в этот день, вы получите либо ошибку, либо тихое поведение в зависимости от вашей библиотеки.
- Случай «возвращения на зиму»: Часы уходят с 2:00 до 1:00. Время 1:30 теперь происходит дважды. Без дополнительного контекста (флага fold), вы не можете определить, какое из этих двух случаев вы имеете в виду.
И правила перехода на летнее время меняются. Государства и штаты США изменили, отменили или модифицировали свои правила в течение последних лет. Поведение вашего кода зависит от наличия обновлённой версии tzdata — это вопрос операционной поддержки, а не только разработчика.
3. Не все смещения — целые часы
Индия — UTC+5:30. Непал — UTC+5:45. Части Австралии — UTC+9:30. Иран — UTC+3:30 зимой и UTC+4:30 летом. Если ваша система предполагает, что часовые пояса всегда на полчаса, она будет безвозвратно искажать временные метки для миллионов пользователей.
4. «Локальное время» на сервере не имеет смысла
Серверы имеют системные часы. Эти часы имеют настроенный часовой пояс — часто UTC, иногда то, что по умолчанию задал хостинг-провайдер, иногда то, что настроил системный администратор много лет назад. Код, который вызывает new Date() или datetime.now() без указания пояса, зависит от этой настройки сервера. Разные среды дают разные результаты. Это ошибка, которая может возникнуть на каждом развертывании.
Правильный подход, по языку
JavaScript / TypeScript
Встроенная Date объект в JavaScript — это тонкая обёртка вокруг временного метка в миллисекундах UTC. Он выглядит дружелюбно, но не является таким. Избегайте ручного форматирования — используйте API для отображения или обращайтесь к библиотеке. Intl.DateTimeFormat Современный стандарт — Temporal
— предложение TC39, которое появилось в Chrome 121 и скоро появится во всех основных средах выполнения. Оно имеет первоклассную поддержку часовых поясов и является правильным долгосрочным решением: Если вы ещё не можете использовать Temporal,
// Store an instant (UTC-equivalent)
const meeting = Temporal.Instant.from("2025-06-15T14:00:00Z");
// Display in a specific zone
const nyTime = meeting.toZonedDateTimeISO("America/New_York");
console.log(nyTime.toString()); // 2025-06-15T10:00:00-04:00[America/New_York]
// Convert to Tokyo time
const tokyoTime = meeting.toZonedDateTimeISO("Asia/Tokyo");
console.log(tokyoTime.toString()); // 2025-06-16T23:00:00+09:00[Asia/Tokyo]
date-fns-tz в паре с date-fns — надёжный выбор. Luxon — ещё один надёжный вариант. Moment.js — полностью функциональный, но больше не поддерживается — перейдите от него. модуль отличает между «наивными» датами (без информации о часовом поясе) и «осознанными» датами (с tzinfo). Наивные даты — это ловушка — они выглядят как действительные времена, но не несут смысла между системами.
Питон
Python’s datetime Всегда используйте осознанные даты. Используйте модуль
(Python 3.9+) для поддержки названных часовых поясов: zoneinfo Для Python 3.8 и ранее, используйте библиотеку — но будьте осторожны с
from datetime import datetime
from zoneinfo import ZoneInfo
# Aware datetime — always do this
utc_time = datetime(2025, 6, 15, 14, 0, 0, tzinfo=ZoneInfo("UTC"))
# Convert to New York time
ny_time = utc_time.astimezone(ZoneInfo("America/New_York"))
print(ny_time) # 2025-06-15 10:00:00-04:00
# Never do this — naive datetime, meaningless
bad = datetime(2025, 6, 15, 14, 0, 0) # what zone is this?
‘s localize/normalize API, который имеет ловушки, pytz избегающие. pytzКлассы поддерживают названные часовые пояса нативно через zoneinfo . Предпочитайте
PHP
PHP’s DateTime и DateTimeImmutable — она безопаснее, потому что изменения возвращают новые объекты, а не изменяют объекты в месте. DateTimeZoneБазы данных / SQL DateTimeImmutable Работа с часовыми поясами в базах данных — это собственная ловушка.
$utc = new DateTimeImmutable('2025-06-15T14:00:00', new DateTimeZone('UTC'));
// Convert to Sydney time
$sydney = $utc->setTimezone(new DateTimeZone('Australia/Sydney'));
echo $sydney->format('Y-m-d H:i:s T'); // 2025-06-16 00:00:00 AEST
// Store timestamps as ISO 8601 strings or Unix timestamps
echo $utc->getTimestamp(); // 1749996000
PostgreSQL:
(timestamp with time zone) — хранит всё в UTC и преобразует на выходе. Никогда не используйте
- (без часового пояса) для пользовательских данных; оно хранит то, что вы ему передали, без преобразования. Используйте
TIMESTAMPTZMySQL:TIMESTAMPтип — наивный (без пояса). Используйте - если вы хотите хранить в UTC, но обратите внимание, что его диапазон ограничен 2038 годом. Для новых схем хранения в формате The
DATETIMEв формате ISO 8601 или как Unix-время вTIMESTAMPчасто безопаснее.VARCHARSQLite:BIGINTне имеет встроенного типа часовых поясов. Храните в виде текста ISO 8601 ( - ) или как целое число Unix. Обрабатывайте преобразование в коде приложения. Независимо от используемой базы данных, устанавливайте сессионный часовой пояс сервера явно, а не полагайтесь на настройки по умолчанию.
2025-06-15T14:00:00ZПрактические правила, которые действительно работают
После рассмотрения теории, вот конкретные правила, которые предотвращают большинство ошибок с часовыми поясами:
Храните UTC повсюду.
Каждый временной метка в вашей базе данных должна быть в UTC. Никаких исключений для «это используется только внутренне».
- Используйте названные часовые пояса, а не смещения. Храните
- вместе с UTC-меткой при необходимости восстановить исходное локальное время. Смещение само по себе не может быть восстановлено после перехода на летнее время. Никогда не вызывайте «сейчас» без пояса.
America/Chicago— всегда передавайте явный аргумент пояса или немедленно привязывайте его. - Преобразуйте в локальное время на последнем этапе.
datetime.now(),new Date(),time()Выполняйте все вычисления с датами в UTC. Преобразовывайте в локальное время пользователя только перед отображением. - Поддерживайте tzdata в актуальном состоянии. Правила часовых поясов меняются. Делайте обновления tzdata частью регулярного управления зависимостями.
- Тестируйте в периоды перехода на летнее время. Две опасные даты каждый год (переход весной, возвращение на зиму) должны быть в вашем тестовом наборе, если вы обрабатываете планирование или данные, зависящие от времени.
- Запрашивайте у пользователей их часовой пояс явно. Не полагайтесь на геолокацию по IP или предположения из браузера для чего-либо важного. Показывайте выбор часового пояса; сохраняйте результат.
- Слово о Unix-временных метках Unix-временные метки — секунды с 1970-01-01T00:00:00Z — не зависят от часовых поясов по определению. Это полностью допустимый формат хранения и особенно полезны в журналах, API и кэшах, где вы хотите иметь однозначное число.
Проблема: Unix-временные метки не несут намерения пользователя.
Если кто-то бронирует рейс на «воскресенье утром в Лондоне», а вы храните только Unix-временную метку, вы потеряли факт того, что имелось в виду Лондон. Когда рейс перебронируется через три месяца и Лондон перешёл в или из летнего времени, вы можете отобразить неправильное локальное время. Храните пояс вместе с временной меткой каждый раз, когда важно сохранить намерение пользователя.
Самый сложный случай: повторяющиеся события Повторяющиеся события — еженедельные встречи, ежемесячные циклы оплаты, ежедневные напоминания — раскрывают каждый крайний случай с часовым поясом одновременно.Рассмотрим правило «каждое понедельник в 9 утра» для пользователя в Лос-Анджелесе. Храните ли вы 9 утра в Пацифике? Что происходит, когда они путешествуют в Токио? Что происходит, когда часы прыгают и этот понедельник выпадает на дату перехода?
Единственное правильное решение:
Храните правило повторения как время по стенду + названный часовой пояс: «понедельник 09:00 America/Los_Angeles»
Вычисляйте следующее UTC-время при планировании, а не при создании правила
Пересчитывайте после любого перехода на летнее время, который попадает в окно повторения
- Библиотеки, такие как
- (JS) и
- (Python), правильно обрабатывают это при наличии правильного контекста часовых поясов. Ручное написание этой логики — надёжный путь к тонким ошибкам, которые проявляются через несколько месяцев.
Библиотеки, такие как rrule.js (JS) и dateutil.rrule (Python) корректно обрабатывают это при наличии правильного контекста часового пояса. Ручная реализация этой логики — надёжный путь к тонким ошибкам, которые проявляются через месяцы.
Установите наши расширения
Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска
恵 Табло результатов прибыло!
Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!
Подписаться на новости
все Новые поступления
всеОбновлять: Наш последний инструмент был добавлен 14 мая 2026
