Конвертация из Date в js-joda типы

Объект Date в JavaScript представляет момент времени как количество миллисекунд, прошедших с 1 января 1970 года по UTC. Внутренне это всегда UTC-ориентированная модель, однако большинство методов чтения и форматирования используют локальную временную зону среды выполнения, что часто приводит к неоднозначности при работе с календарными типами.

Библиотека js-joda вводит строгую модель времени, разделяя:

  • Instant — точный момент времени в UTC
  • LocalDate — календарная дата без времени и зоны
  • LocalDateTime — дата и время без зоны
  • ZonedDateTime — дата, время и временная зона

Преобразование из Date в эти типы требует явного выбора целевой семантики, поскольку один Date может быть интерпретирован по-разному в зависимости от контекста.


Базовая точка преобразования: Instant

Наиболее корректным промежуточным представлением для любого Date является Instant.

Преобразование через epoch milliseconds

import { Instant } from '@js-joda/core';

const date = new Date();
const instant = Instant.ofEpochMilli(date.getTime());

getTime() возвращает количество миллисекунд UTC-эпохи, что напрямую соответствует Instant.

Прямое преобразование через ofEpochMilli

Instant является наиболее близким аналогом Date, так как оба оперируют абсолютным временем без календарной семантики.


Использование Instant как универсального источника

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

Преобразование в ZonedDateTime

import { Instant, ZoneId, ZonedDateTime } from '@js-joda/core';

const date = new Date();
const instant = Instant.ofEpochMilli(date.getTime());

const zone = ZoneId.of('Europe/Berlin');
const zonedDateTime = ZonedDateTime.ofInstant(instant, zone);

ZonedDateTime фиксирует момент времени и интерпретирует его в рамках конкретной временной зоны.


Преобразование в LocalDateTime

LocalDateTime требует комбинации момента времени и зоны, поскольку сам по себе не хранит информацию о часовом поясе.

import { Instant, ZoneId, LocalDateTime } from '@js-joda/core';

const date = new Date();
const instant = Instant.ofEpochMilli(date.getTime());

const zone = ZoneId.systemDefault();
const localDateTime = LocalDateTime.ofInstant(instant, zone);

Важная особенность

LocalDateTime зависит от переданной зоны. Один и тот же Date даст разные значения LocalDateTime при разных ZoneId.


Преобразование в LocalDate

LocalDate извлекается из Instant с учётом временной зоны и отбрасыванием времени суток.

import { Instant, ZoneId, LocalDate } from '@js-joda/core';

const date = new Date();
const instant = Instant.ofEpochMilli(date.getTime());

const zone = ZoneId.of('Asia/Almaty');
const localDate = LocalDate.ofInstant(instant, zone);

Семантика операции

  • время суток игнорируется
  • учитывается переход через границы суток в выбранной зоне
  • результат зависит от ZoneId

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

Часто требуется преобразование в локальную системную зону окружения выполнения.

import { Instant, ZoneId, LocalDateTime } from '@js-joda/core';

const date = new Date();
const instant = Instant.ofEpochMilli(date.getTime());

const localDateTime = LocalDateTime.ofInstant(
  instant,
  ZoneId.systemDefault()
);

Системная зона не фиксирована и может различаться между сервером и клиентом, что делает такой подход контекстно-зависимым.


Альтернативный путь через Instant.parse не применяется

В js-joda Instant.parse работает со строками ISO-8601 и не предназначен для Date. Попытка преобразования через строковое представление date.toISOString() возможна, но менее эффективна и добавляет лишнюю сериализацию.

import { Instant } from '@js-joda/core';

const date = new Date();
const instant = Instant.parse(date.toISOString());

Такой подход корректен, но уступает ofEpochMilli по производительности и ясности модели.


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

Потеря информации о формате Date

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

  • явной временной зоны
  • календарной структуры (год/месяц/день как отдельные сущности)
  • различия между локальным временем и UTC в виде отдельного состояния

js-joda, напротив, строго разделяет эти аспекты, поэтому преобразование всегда требует выбора интерпретации.


Частые ошибки при конвертации

Игнорирование зоны при создании LocalDate

const localDate = LocalDate.ofInstant(
  Instant.ofEpochMilli(date.getTime())
);

Такой код некорректен: отсутствует ZoneId, что делает преобразование невозможным или логически неполным.


Использование LocalDateTime как заменителя Date

LocalDateTime не является абсолютным временем. Его использование вместо Instant приводит к неоднозначности:

  • одинаковые значения могут соответствовать разным моментам времени
  • отсутствует информация о смещении

Двойная интерпретация времени

const instant = Instant.ofEpochMilli(new Date(date).getTime());

Избыточное создание Date не изменяет семантику, но добавляет лишний слой преобразования без смысла.


Конвертация массива Date в js-joda типы

Преобразование в Instant

import { Instant } from '@js-joda/core';

const dates = [new Date(), new Date(Date.now() - 1000000)];

const instants = dates.map(d => Instant.ofEpochMilli(d.getTime()));

Преобразование в LocalDate

import { Instant, ZoneId, LocalDate } from '@js-joda/core';

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

const localDates = dates.map(d =>
  LocalDate.ofInstant(Instant.ofEpochMilli(d.getTime()), zone)
);

Конвертация в рамках API слоёв приложения

Слой хранения данных

Рекомендуется использовать Instant как универсальный формат хранения времени.

const stored = Instant.ofEpochMilli(date.getTime());

Слой бизнес-логики

Используются LocalDate или LocalDateTime в зависимости от предметной области.

const businessDate = LocalDate.ofInstant(instant, zone);

Слой отображения

Часто снова требуется ZonedDateTime для корректного форматирования.

const viewTime = ZonedDateTime.ofInstant(instant, zone);

Работа с Unix timestamp

Date и js-joda Instant одинаково опираются на Unix epoch, что делает преобразование детерминированным.

const timestamp = Date.now();

const instant = Instant.ofEpochMilli(timestamp);

Обратное преобразование:

const millis = instant.toEpochMilli();
const date = new Date(millis);

Особенности поведения при смене временной зоны

Один и тот же Instant может давать разные календарные представления:

const instant = Instant.ofEpochMilli(Date.now());

const moscow = LocalDateTime.ofInstant(instant, ZoneId.of('Europe/Moscow'));
const tokyo = LocalDateTime.ofInstant(instant, ZoneId.of('Asia/Tokyo'));

Различие проявляется в:

  • дате (возможен сдвиг суток)
  • времени суток
  • значении локальных календарных границ

Использование промежуточных функций обёртки

В реальных проектах часто вводятся функции адаптации:

import { Instant } from '@js-joda/core';

export const fromDateToInstant = (date) =>
  Instant.ofEpochMilli(date.getTime());
import { ZoneId, LocalDate } from '@js-joda/core';

export const fromDateToLocalDate = (date, zone = ZoneId.systemDefault()) =>
  LocalDate.ofInstant(Instant.ofEpochMilli(date.getTime()), zone);

Такая инкапсуляция снижает вероятность ошибок при работе с зонами и семантикой времени.


Различие подходов: прямое и строковое преобразование

Через epoch milliseconds

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

Через ISO строку

Instant.parse(date.toISOString());
  • дополнительная сериализация
  • зависимость от формата строки
  • потенциальные накладные расходы

Поведение при некорректных значениях Date

Date может быть:

  • Invalid Date
  • объектом с NaN внутри

Преобразование:

const date = new Date('invalid');

const instant = Instant.ofEpochMilli(date.getTime()); // NaN

Instant в таком случае не может быть корректно создан, и ошибка проявляется на этапе использования значения, а не при создании Date.