Временные зоны — это ложь (и как с ними работать в коде)

Опубликовано

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

Временные зоны — это ложь (и как с ними работать в коде)  1
Реклама · УДАЛИТЬ?

Вы открываете карту. Вы видите вертикальные сечения. «UTC-5 — Восточное время, UTC+1 — Центральное европейское время». Просто, верно?

Неверно. Часовые пояса — это политическое создание, а не географическая реальность — и этот разрыв спасёт вас от многих самых ужасных ошибок в разработке программного обеспечения.

Почему часовые пояса на самом деле — это хаос

Вот что делает ваша операционная система с базой tz на вашей стороне:

  • Дневное экономление времени (DST) — Не все страны его соблюдают, страны меняют правила, и дата переключения зависит от того, где вы находитесь. В США дата переключения изменилась в 2007 году. Египет отменил DST в 2011 году, затем вновь ввёл его, а затем отменил снова.
  • Полу- и четвертьчасовые сдвиги — Индия (UTC+5:30), Непал (UTC+5:45) и части Австралии (UTC+9:30) существуют. Ваше предположение, что сдвиги — целые часы, неверно.
  • Исторические изменения — Россия перешла с UTC+4 на UTC+3 в 2014 году. Самоа пропустила целый день (30 декабря 2011 года), чтобы перейти с одной стороны международной линии даты на другую. Когда вы работаете с историческими метками времени, правильный сдвиг может быть совершенно иным, чем сегодня.
  • Неопределённые времена — Когда часы уходят вперёд, 1:30 утра происходит дважды. Когда часы возвращаются, 2:30 утра вообще не существует. Если вы храните «3 ноября 2024 года в 1:45 утра Восточного времени» без UTC-сдвига, вы получаете неопределённую метку времени.

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

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

Храните и обрабатывайте в UTC. Преобразовывайте в локальное время только при отображении.

Это не спорное предложение. Это общее мнение всех основных баз данных, стандартов API и команд распределённых систем. Если столбец вашей базы данных хранит 2024-11-03 06:45:00 в UTC, вы всегда точно знаете, какое время это представляет — независимо от того, где размещён ваш сервер, в каком часовом поясе находится ваш пользователь или произошло ли DST в этот день.

Момент, когда вы храните 2024-11-03 01:45:00 без UTC-сдвига, вы теряете информацию. Вы создаёте метку времени, которая может означать два разных момента.

Как обрабатывать часовые пояса в JavaScript

Встроенная в JavaScript Date объект хранит время как миллисекунды с начала эпохи Unix (1 января 1970 года UTC) — что хорошо. Проблема в том, что его API неоднозначен и несогласован.

Что избегать

// BAD: Parsing without explicit timezone
const d = new Date('2024-11-03 01:45:00');
// Interpreted as LOCAL time — behavior varies by environment

// BAD: Storing user-facing strings as dates
const meeting = '3:00 PM Thursday';
// This is meaningless without a timezone

Что использовать вместо этого

// GOOD: Always use ISO 8601 with explicit UTC offset
const d = new Date('2024-11-03T06:45:00Z'); // Z = UTC

// GOOD: Display in user's local timezone using Intl API
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  dateStyle: 'full',
  timeStyle: 'short',
});
console.log(formatter.format(d)); // "Sunday, November 3, 2024 at 1:45 AM"

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

import { DateTime } from 'luxon';

// Parse an ISO string and convert to a specific zone
const utc = DateTime.fromISO('2024-11-03T06:45:00Z');
const eastern = utc.setZone('America/New_York');
console.log(eastern.toLocaleString(DateTime.DATETIME_FULL));
// "November 3, 2024, 1:45 AM EDT"

Как обрабатывать часовые пояса в Python

Python’s datetime модуль различает между наивными (без часового пояса) и осознанными (с часовым поясом) объектами даты. Наивные даты — это ловушка — избегайте их в любом коде, который пересекает границы часовых поясов.

from datetime import datetime, timezone
import zoneinfo  # Python 3.9+

# BAD: naive datetime (no timezone info)
d = datetime(2024, 11, 3, 1, 45, 0)

# GOOD: always attach a timezone
d_utc = datetime(2024, 11, 3, 6, 45, 0, tzinfo=timezone.utc)

# Convert to a specific timezone
eastern = zoneinfo.ZoneInfo('America/New_York')
d_eastern = d_utc.astimezone(eastern)
print(d_eastern)  # 2024-11-03 01:45:00-05:00

Для более старых версий Python или сложных задач расписания, python-dateutil и Стрелка — это хорошо поддерживаемые варианты. Основная идея остаётся той же: работайте в UTC, преобразовывайте на границе.

Как обрабатывать часовые пояса в базах данных

Работа с часовыми поясами в базах данных поражает даже опытных разработчиков.

PostgreSQL

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

-- BAD
CREATE TABLE events (created_at TIMESTAMP);

-- GOOD
CREATE TABLE events (created_at TIMESTAMPTZ);

-- Querying across timezones
SELECT created_at AT TIME ZONE 'America/New_York' FROM events;

MySQL

MySQL’s DATETIME с нулевым контекстом часового пояса — то, что вы вводите, то и получаете, без преобразований. TIMESTAMP преобразует в UTC при записи и возвращает в текущий серверский часовой пояс при чтении — но ограничивается датами от 1970 до 2038 года (переполнение Unix-времени). На практике: используйте DATETIME столбцы и убедитесь, что ваша приложение всегда записывает значения в UTC.

Планирование по часовым поясам: скрытая ловушка

Представим, что пользователь планирует еженедельное совещание на «каждое понедельник в 9 утра по времени Нью-Йорка». Вы храните следующее наступление как метку времени в UTC. Хорошо — пока не меняются DST, и теперь метка времени в UTC, которую вы хранили, соответствует 10 утра по времени Нью-Йорка вместо 9 утра.

Решение: не храните абсолютные будущие метки времени для повторяющихся событий. Храните правило локального времени — идентификатор часового пояса плюс локальное время — и вычисляйте следующую метку времени в UTC на уровне выполнения. Таким образом, система корректно пересчитывает после переходов DST.

-- Store the intent, not the absolute moment
CREATE TABLE recurring_events (
  id SERIAL PRIMARY KEY,
  timezone TEXT NOT NULL,       -- 'America/New_York'
  local_time TIME NOT NULL,     -- '09:00:00'
  recurrence TEXT NOT NULL      -- 'WEEKLY:MONDAY'
);
-- Compute next_occurrence_utc in application code at dispatch time

Имена часовых поясов IANA против UTC-сдвигов

Когда вы храните предпочтение пользователя по часовому поясу, храните имя IANA (America/New_York, Europe/London, Asia/Kolkata) — а не UTC-сдвиг (UTC-5, UTC+1). Сдвиги — это снимки; имена IANA кодируют полную историю DST и будущие правила.

America/New_York является UTC-5 в зимний период и UTC-4 в летний период. Если вы храните UTC-5, вы встраиваете неправильный сдвиг в половину года.

Быстрый справочник: правила

  • Храните метки времени в UTC — всегда, без исключений в вашей базе данных или ответах API.
  • Используйте имена часовых поясов IANA — а не UTC-сдвиги — для предпочтений пользователей и повторяющихся расписаний.
  • Никогда не парсите строки дат без явного часового пояса — поведение по умолчанию зависит от среды.
  • Преобразовывайте в локальное время на уровне отображения — а не в бизнес-логике или запросах к базе данных.
  • Для повторяющихся расписаний, храните правило — а не предварительно вычисленное время в UTC — чтобы переходы DST не повреждали будущие события.
  • Тестируйте с крайними случаями — час перехода DST, даты вблизи международной линии даты и исторические метки времени в регионах, которые изменили сдвиг.
Хотите убрать рекламу? Откажитесь от рекламы сегодня

Установите наши расширения

Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска

в Расширение Chrome в Расширение края в Расширение Firefox в Расширение Opera

Табло результатов прибыло!

Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!

Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?

новости с техническими моментами

Примите участие

Помогите нам продолжать предоставлять ценные бесплатные инструменты

Купи мне кофе
Реклама · УДАЛИТЬ?