Объект Date в JavaScript представляет момент времени как
количество миллисекунд, прошедших с 1 января 1970 года по UTC. Внутренне
это всегда UTC-ориентированная модель, однако большинство методов чтения
и форматирования используют локальную временную зону среды выполнения,
что часто приводит к неоднозначности при работе с календарными
типами.
Библиотека js-joda вводит строгую модель времени, разделяя:
Преобразование из Date в эти типы требует явного выбора
целевой семантики, поскольку один Date может быть
интерпретирован по-разному в зависимости от контекста.
Наиболее корректным промежуточным представлением для любого
Date является Instant.
import { Instant } from '@js-joda/core';
const date = new Date();
const instant = Instant.ofEpochMilli(date.getTime());
getTime() возвращает количество миллисекунд UTC-эпохи,
что напрямую соответствует Instant.
Instant является наиболее близким аналогом
Date, так как оба оперируют абсолютным временем без
календарной семантики.
После получения Instant возможна дальнейшая
трансформация в любой календарный тип при наличии зоны.
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 требует комбинации момента времени и зоны,
поскольку сам по себе не хранит информацию о часовом поясе.
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 извлекается из 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Часто требуется преобразование в локальную системную зону окружения выполнения.
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()
);
Системная зона не фиксирована и может различаться между сервером и клиентом, что делает такой подход контекстно-зависимым.
В 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 не содержит:
js-joda, напротив, строго разделяет эти аспекты, поэтому преобразование всегда требует выбора интерпретации.
const localDate = LocalDate.ofInstant(
Instant.ofEpochMilli(date.getTime())
);
Такой код некорректен: отсутствует ZoneId, что делает
преобразование невозможным или логически неполным.
LocalDateTime не является абсолютным временем. Его
использование вместо Instant приводит к
неоднозначности:
const instant = Instant.ofEpochMilli(new Date(date).getTime());
Избыточное создание Date не изменяет семантику, но
добавляет лишний слой преобразования без смысла.
import { Instant } from '@js-joda/core';
const dates = [new Date(), new Date(Date.now() - 1000000)];
const instants = dates.map(d => Instant.ofEpochMilli(d.getTime()));
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)
);
Рекомендуется использовать Instant как универсальный
формат хранения времени.
const stored = Instant.ofEpochMilli(date.getTime());
Используются LocalDate или LocalDateTime в
зависимости от предметной области.
const businessDate = LocalDate.ofInstant(instant, zone);
Часто снова требуется ZonedDateTime для корректного
форматирования.
const viewTime = ZonedDateTime.ofInstant(instant, zone);
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);
Такая инкапсуляция снижает вероятность ошибок при работе с зонами и семантикой времени.
Instant.parse(date.toISOString());
Date может быть:
Invalid DateNaN внутриПреобразование:
const date = new Date('invalid');
const instant = Instant.ofEpochMilli(date.getTime()); // NaN
Instant в таком случае не может быть корректно создан, и
ошибка проявляется на этапе использования значения, а не при создании
Date.