Разделение понятий даты, времени и временной зоны

В стандартном объекте Date в JavaScript одновременно смешиваются несколько понятий:

  • календарная дата;
  • время суток;
  • временная зона;
  • момент времени относительно UTC.

Такой подход часто приводит к ошибкам:

const date = new Date('2025-03-10');

console.log(date.toString());
console.log(date.toISOString());

В разных часовых поясах результат может отличаться. Особенно проблемными становятся:

  • серверные приложения;
  • международные системы;
  • расписания;
  • финансовые операции;
  • бронирование;
  • работа с API.

Библиотека js-joda реализует подход из Java java.time, где каждая сущность времени представлена отдельным типом.

Это фундаментальный принцип библиотеки:

  • дата — отдельно;
  • время — отдельно;
  • временная зона — отдельно;
  • абсолютный момент времени — отдельно.

Такое разделение устраняет большую часть ошибок, связанных с временем.


LocalDate — только дата

LocalDate хранит исключительно календарную дату:

  • год;
  • месяц;
  • день.

Без:

  • времени;
  • часового пояса;
  • UTC;
  • смещения.

Создание LocalDate

const { LocalDate } = require('@js-joda/core');

const date = LocalDate.of(2025, 5, 24);

console.log(date.toString());

Результат:

2025-05-24

Использование LocalDate

LocalDate подходит для:

  • дней рождения;
  • праздников;
  • рабочих дней;
  • бухгалтерских дат;
  • дедлайнов;
  • дат документов.

Важное свойство LocalDate

Дата не зависит от временной зоны.

const date = LocalDate.parse('2025-05-24');

console.log(date);

Во всех странах это будет одна и та же дата.


LocalTime — только время суток

LocalTime хранит:

  • часы;
  • минуты;
  • секунды;
  • наносекунды.

Без даты и временной зоны.

Создание LocalTime

const { LocalTime } = require('@js-joda/core');

const time = LocalTime.of(14, 30, 15);

console.log(time.toString());

Результат:

14:30:15

Когда использовать LocalTime

Подходит для:

  • времени открытия магазина;
  • расписания занятий;
  • времени будильника;
  • ежедневных событий;
  • cron-подобных задач.

LocalDateTime — дата и время без зоны

LocalDateTime объединяет:

  • дату;
  • время.

Но всё ещё не содержит временную зону.

Создание LocalDateTime

const { LocalDateTime } = require('@js-joda/core');

const dateTime = LocalDateTime.of(
    2025,
    5,
    24,
    14,
    30
);

console.log(dateTime.toString());

Результат:

2025-05-24T14:30

Что означает LocalDateTime

Это локальное представление времени.

Например:

«24 мая 2025 года в 14:30»

Но без информации:

  • в какой стране;
  • в каком часовом поясе;
  • относительно UTC.

Проблема неоднозначности LocalDateTime

Одно и то же локальное время может существовать в разных зонах одновременно.

Например:

2025-05-24T14:30

может означать:

  • 14:30 в Токио;
  • 14:30 в Берлине;
  • 14:30 в Алматы.

Это разные реальные моменты времени.

Поэтому LocalDateTime нельзя использовать как абсолютную временную метку.


ZonedDateTime — дата, время и временная зона

ZonedDateTime хранит:

  • дату;
  • время;
  • временную зону.

Создание ZonedDateTime

const {
    ZonedDateTime,
    ZoneId
} = require('@js-joda/core');

require('@js-joda/timezone');

const zoned = ZonedDateTime.of(
    2025,
    5,
    24,
    14,
    30,
    0,
    0,
    ZoneId.of('Asia/Almaty')
);

console.log(zoned.toString());

Результат:

2025-05-24T14:30+05:00[Asia/Almaty]

Что такое временная зона

Временная зона — это не просто смещение.

Неверный подход:

+05:00

Полноценная зона содержит:

  • правила перехода на летнее время;
  • исторические изменения;
  • региональные настройки.

Например:

Europe/Berlin
America/New_York
Asia/Tokyo

ZoneId и ZoneOffset

В библиотеке разделяются два понятия.

ZoneOffset

Фиксированное смещение:

+05:00
-03:00
UTC

Пример:

const { ZoneOffset } = require('@js-joda/core');

const offset = ZoneOffset.of('+05:00');

ZoneId

Полноценная временная зона:

const { ZoneId } = require('@js-joda/core');

const zone = ZoneId.of('Europe/Berlin');

ZoneId учитывает:

  • DST;
  • смену правил;
  • изменения законодательства.

Instant — абсолютный момент времени

Instant представляет точку времени в UTC.

Это самый точный и безопасный тип для:

  • хранения в БД;
  • логирования;
  • обмена между сервисами;
  • API;
  • распределённых систем.

Создание Instant

const { Instant } = require('@js-joda/core');

const instant = Instant.now();

console.log(instant.toString());

Пример:

2025-05-24T09:20:15.120Z

Суффикс Z означает UTC.


Связь между типами

LocalDate → LocalDateTime

const {
    LocalDate,
    LocalTime
} = require('@js-joda/core');

const date = LocalDate.of(2025, 5, 24);
const time = LocalTime.of(14, 30);

const dateTime = date.atTime(time);

console.log(dateTime.toString());

LocalDateTime → ZonedDateTime

const {
    LocalDateTime,
    ZoneId
} = require('@js-joda/core');

require('@js-joda/timezone');

const local = LocalDateTime.parse(
    '2025-05-24T14:30'
);

const zoned = local.atZone(
    ZoneId.of('Asia/Almaty')
);

console.log(zoned.toString());

ZonedDateTime → Instant

const instant = zoned.toInstant();

console.log(instant.toString());

Почему нельзя хранить дату рождения как Instant

Дата рождения — это календарная дата.

Неверный подход:

Instant.parse('1995-08-10T00:00:00Z')

Проблемы:

  • смещение даты;
  • различия часовых поясов;
  • неожиданные преобразования.

Правильный тип:

LocalDate

Почему нельзя хранить расписание как Instant

Допустим:

Магазин открывается в 09:00.

Это локальное время, а не глобальный момент.

Если хранить как Instant, то после смены часового пояса возникнут ошибки.

Правильный тип:

LocalTime

Почему Instant не подходит для пользовательского интерфейса

Пользователю нужен локальный формат времени.

Instant не содержит:

  • временную зону;
  • локальное представление.

Поэтому для UI обычно выполняется преобразование:

const zoned = instant.atZone(
    ZoneId.of('Asia/Almaty')
);

Работа с часовыми поясами

Подключение timezone-модуля

Для поддержки зон требуется:

require('@js-joda/timezone');

Без этого доступны только фиксированные offset-зоны.


Конвертация между зонами

const {
    ZonedDateTime,
    ZoneId
} = require('@js-joda/core');

require('@js-joda/timezone');

const tokyo = ZonedDateTime.now(
    ZoneId.of('Asia/Tokyo')
);

const berlin = tokyo.withZoneSameInstant(
    ZoneId.of('Europe/Berlin')
);

console.log(tokyo.toString());
console.log(berlin.toString());

Разница between withZoneSameInstant и withZoneSameLocal

withZoneSameInstant

Сохраняет реальный момент времени.

Меняется локальное отображение.

const berlin = tokyo.withZoneSameInstant(
    ZoneId.of('Europe/Berlin')
);

withZoneSameLocal

Сохраняет локальное время.

Меняется фактический момент времени.

const berlin = tokyo.withZoneSameLocal(
    ZoneId.of('Europe/Berlin')
);

Это крайне важное различие.


Летнее время и проблемы DST

DST (Daylight Saving Time) создаёт множество сложностей.

Например, время может:

  • повторяться;
  • пропадать;
  • перескакивать.

Несуществующее время

Во время перехода на летнее время некоторые часы исчезают.

Пример:

2025-03-30 02:30

в некоторых странах не существует.

js-joda умеет корректно обрабатывать такие случаи.


Повторяющееся время

При возврате с летнего времени одно и то же локальное время встречается дважды.

Например:

2025-10-26 02:30

может соответствовать двум разным Instant.


Почему Date плохо справляется с часовыми поясами

Встроенный Date:

  • зависит от среды выполнения;
  • смешивает UTC и локальное время;
  • имеет неявные преобразования;
  • использует мутирующий API.

Пример проблемы:

const date = new Date();

date.setHours(date.getHours() + 1);

Объект изменяется напрямую.


Неизменяемость объектов в js-joda

Все объекты библиотеки immutable.

Пример:

const {
    LocalDate
} = require('@js-joda/core');

const original = LocalDate.parse('2025-05-24');

const modified = original.plusDays(5);

console.log(original.toString());
console.log(modified.toString());

Результат:

2025-05-24
2025-05-29

Исходный объект не изменился.


Архитектурное значение разделения типов

Разделение типов помогает:

  • избегать ошибок;
  • делать API предсказуемым;
  • явно выражать намерения;
  • строить надёжные системы времени.

Практическое соответствие типов задачам

Задача Тип
День рождения LocalDate
Время открытия LocalTime
Локальное событие LocalDateTime
Международное событие ZonedDateTime
Timestamp в БД Instant

Типичная архитектура хранения времени

В базе данных

Обычно хранится:

Instant

или UTC timestamp.


На сервере

Используются:

  • Instant
  • ZonedDateTime

В интерфейсе

Используются:

  • LocalDate
  • LocalTime
  • ZonedDateTime

Пример полной цепочки

Создание события пользователем

Пользователь вводит:

  • дату;
  • время;
  • свою временную зону.
const local = LocalDateTime.of(
    2025,
    5,
    24,
    19,
    0
);

const zoned = local.atZone(
    ZoneId.of('Asia/Almaty')
);

const instant = zoned.toInstant();

Сохранение в БД

instant.toString()

Например:

2025-05-24T14:00:00Z

Отображение другому пользователю

const berlinTime = instant.atZone(
    ZoneId.of('Europe/Berlin')
);

Каждый пользователь увидит своё локальное время.


Отделение бизнес-времени от машинного времени

Это один из главных принципов современной работы со временем.

Бизнес-время

То, как время воспринимает человек:

  • календарные даты;
  • локальные часы;
  • расписания.

Типы:

  • LocalDate
  • LocalTime
  • LocalDateTime

Машинное время

То, как время хранится системой:

  • UTC;
  • timestamp;
  • абсолютные моменты.

Тип:

Instant

Ошибки при смешивании типов

Хранение локального времени как UTC

Ошибка:

2025-05-24T09:00Z

при попытке представить:

«магазин открывается в 09:00»

После смены зоны время станет неверным.


Использование LocalDateTime для глобальных событий

Например:

2025-05-24T18:00

Непонятно:

  • в какой зоне;
  • для какого региона;
  • какой это реальный момент времени.

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

Явность

Тип объекта показывает смысл данных.


Безопасность

Меньше скрытых преобразований.


Предсказуемость

Поведение не зависит от локальной среды.


Масштабируемость

Система легче развивается при международной работе.


Основные рекомендации

Использовать LocalDate для:

  • документов;
  • дней рождения;
  • праздников;
  • календарей.

Использовать LocalTime для:

  • расписаний;
  • времени открытия;
  • повторяющихся ежедневных событий.

Использовать ZonedDateTime для:

  • международных встреч;
  • событий с часовыми поясами;
  • перелётов;
  • конференций.

Использовать Instant для:

  • БД;
  • логов;
  • синхронизации;
  • API;
  • серверных timestamp.