Соответствие типов времени реальным задачам с датами: семь примеров с пояснениями
Разберитесь с семью повседневными задачами, связанными с датами и временем, с помощью библиотеки JavaScript Temporal: от простых дат и проблем с переполнением месяцев до часовых поясов, арифметики с переходом на летнее время и проверки данных.
Источники: JavaScript Temporal: 7 Date and Time Problems Explained Simply
Требования к датам обычно кажутся простыми: продлить подписку на месяц, показать встречу в часовом поясе коллеги, подсчитать дни между двумя датами. Осложнения появляются позже, когда оказывается, что февраль имеет меньше дней, изменение времени сумерек делает «завтра» расположенным через 23 часа, или когда необходимо сохранять дату рождения, у которой вообще нет часового пояса. API Temporal в JavaScript решает эту проблему, предоставляя каждому типу значений свой собственный тип, так что сам тип указывает на значение этого значения. Семь приведённых ниже рабочих примеров показывают, какой тип подходит для каждой задачи, где могут возникнуть неожиданности из-за значений по умолчанию, и какие решения Temporal намеренно оставляет на усмотрение вашего приложения.
Настройка среды для запуска примеров
Нативная поддержка всё ещё внедряется. На момент написания статьи MDN указывает, что Temporal доступен ограниченно, поэтому перед использованием встроенной реализации убедитесь в наличии поддержки в выбранных браузерах и версии Node.js. На странице справки MDN по Temporal отслеживается текущий статус совместимости.
Чтобы экспериментировать без нативной поддержки, установите Temporal polyfill в пустую папку проекта. Для этого потребуются Node.js и npm:
mkdir temporal-demo
cd temporal-demo
npm init -y
npm install @js-temporal/polyfill@0.5.1
Создайте файл с названием demo.mjs и начните его с этого импорта:
import { Temporal } from "@js-temporal/polyfill";
Добавьте любой из приведённых ниже примеров после импорта и запустите файл:
node demo.mjs
Во всех примерах предполагается одинаковый способ импорта. Polyfill экспортирует локальную связь Temporal и не устанавливает глобальную, поэтому каждый файл, который его использует, должен выполнять операцию импорта. Версия фиксирована на 0.5.1, поскольку именно под нее были написаны примеры; перед использованием в реальном проекте проверьте версии polyfill.
1. Хранение даты календаря без времени и часового пояса
Дата рождения или срок выполнения контракта часто не имеет значимого времени суток. Если требование заключается просто в «15 октября», указание полуночи и часового пояса создает ложную информацию, и именно эта ложная информация позже приводит к смещению даты на один день при ее отображении в другом месте. Temporal.PlainDate представляет только саму дату:
const deadline = Temporal.PlainDate.from("2026-10-15");
Его поля — это простые числа, и он сериализуется обратно в строку формата ISO:
console.log(deadline.year); // 2026
console.log(deadline.month); // 10
console.log(deadline.day); // 15
console.log(deadline.toString()); // 2026-10-15
У этого значения нет часов, нет смещения по UTC и нет часовой зоны. По умолчанию используется календарь ISO, и в отличие от устаревшего объекта Date месяцы нумеруются от 1 до 12, а не начинаются с нуля.
Имейте в виду этот ограничение: объект PlainDate не фиксирует конкретный момент в абсолютном времени. Если крайний срок действительно означает «17:00 в Дели», вам также нужны время и часовая зона, для чего и предназначен объект ZonedDateTime.
2. Получение новых дат без изменения исходных
Общедоступные изменяемые даты — классическая причина ошибок: одна часть кода изменяет значение, которое ещё читается другой частью. Объекты времени являются неизменяемыми, и арифметические операции всегда возвращают новый объект:
const originalDate = Temporal.PlainDate.from("2026-10-15");
const extendedDate = originalDate.add({ days: 7 });
Логирование обоих значений показывает, что исходное значение осталось нетронутым:
console.log(originalDate.toString()); // 2026-10-15
console.log(extendedDate.toString()); // 2026-10-22
Это означает, что вы можете сохранять первоначальный срок и расширенный срок рядом друг с другом, не делая предварительной копии ничего.
Однако существует распространенная ошибка: вызов originalDate.add({ days: 7 }) в отдельной строке с ожиданием, что originalDate изменится. Это не происходит. Необходимо присвоить результат или вернуть его.
3. Определите, что означает «через месяц»
Добавление месяца к 31 января не имеет единственного правильного решения, поскольку 31 февраля не существует. Вашему приложению нужно правило. По умолчанию в Temporal результат ограничивается ближайшей действительной датой. Начните с конца января:
const januaryEnd = Temporal.PlainDate.from("2026-01-31");
Добавление одного месяца приводит к тому, что дата ограничивается концом февраля:
const nextMonth = januaryEnd.add({ months: 1 });console.log(nextMonth.toString()); // 2026-02-28
Когда молчаливое изменение даты недопустимо, попросите Temporal выбросить исключение, передав параметр overflow: „reject“. В качестве примера рассмотрим дату выставления счета:
const billingDate = Temporal.PlainDate.from("2026-01-31");
Отклонение проявляется в виде исключения RangeError, которое код ловит и затем выбрасывает любые другие ошибки:
try {
billingDate.add({ months: 1 }, { overflow: "reject" });
} catch (error) {
if (!(error instanceof RangeError)) throw error;
console.log("That day does not exist in the next month.");
}
Ни один из вариантов не является частью вашей политики выставления счетов; каждый из них — лишь механизм для её реализации. Также следует учитывать эффект кумуляции: если дата 31 января ограничивается до 28 февраля, добавление ещё одного месяца приведет к 28 марта, а не к 31 марту, поэтому график, формируемый путём постоянного добавления месяца к предыдущей дате, будет смещаться. Если правило заключается в использовании «последнего дня каждого месяца», необходимо явно моделировать это правило, например, всегда вычисляя дату исходя из первоначальной даты или используя количество дней в месяце, вместо последовательного добавления месяцев.
4. Отображение одного момента в нескольких часовых поясах
Временная метка события обозначает конкретный момент. Местная дата и время на экране пользователя зависят от его местоположения. Temporal.Instant представляет сам момент:
const meeting = Temporal.Instant.from("2026-10-15T09:00:00Z");
Преобразование его в часовые пояса с именами позволяет получить местное время в каждом регионе, а возвращение к исходному формату показывает, что оба значения всё ещё относятся к одному и тому же моменту:
const delhi = meeting.toZonedDateTimeISO("Asia/Kolkata");
const london = meeting.toZonedDateTimeISO("Europe/London");console.log(delhi.toString());
// 2026-10-15T14:30:00+05:30[Asia/Kolkata]console.log(london.toString());
// 2026-10-15T10:00:00+01:00[Europe/London]console.log(delhi.toInstant().equals(london.toInstant()));
// true
Местные представления различаются, но сам момент остается идентичным. (Строки в этом фрагменте кода должны быть отдельными предложениями.)
Используйте часовые пояса с именами, такие как Europe/London, вместо фиксированных смещений. Часовой пояс с именем содержит правила изменения его смещения в течение года; жестко заданное значение +01:00 относится только к Лондону в летнее время.
Именно здесь начинается множество ошибок: смещение — это не часовой пояс. Смещение указывает на разницу с UTC в определенный момент времени, тогда как часовой пояс задает правила, определяющие эту разницу в любой момент.
Приведенные выше строки предназначены для работы самих машин, включая аннотацию часового пояса в квадратных скобках. Для отображения пользователю значение необходимо сформатировать в соответствии с их локалом, например с помощью toLocaleString() или Intl.DateTimeFormat, а не выводить его в сыром виде.
5. Разделяйте понятия «завтра» и «через 24 часа»
Большую часть года эти выражения дают одинаковый результат. Однако в периоды смены времени это не так. Возьмем полдень в Нью-Йорке 7 марта 2026 года, за день до перевода часов вперед:
const beforeClockChange = Temporal.ZonedDateTime.from(
"2026-03-07T12:00:00-05:00[America/New_York]"
);
Добавление одного календарного дня и добавление 24 часов приводят к разным местным временам:
const tomorrow = beforeClockChange.add({ days: 1 });
const after24Hours = beforeClockChange.add({ hours: 24 });console.log(tomorrow.toString());
// 2026-03-08T12:00:00-04:00[America/New_York]console.log(after24Hours.toString());
// 2026-03-08T13:00:00-04:00[America/New_York]
Арифметика календаря сохраняет время по настенным часам в полдень. Арифметика исчисления пройденного времени учитывает ровно 24 часа, и поскольку часы сдвинулись на один час за ночь, результат оказывается в 13:00.
Каждый из этих подходов подходит для разных требований. Ежедневная встреча обычно означает «тот же местный временной интервал завтра», поэтому необходимо добавлять дни. Токен или бронирование, срок действия которого истекает ровно через 24 часа, должны учитывать добавление часов. Функция Temporal может вычислять любой из вариантов; вам нужно решить, какое значение требуется вашей функциональности, и стоит записать это решение.
6. Отклонять недействительные даты и отсутствующие местные времена
При проверке вводимых данных формы невозможная дата должна вызывать ошибку, а не тихо преобразовываться в другую дату. Для объектных данных необходимо запросить строгую обработку с параметром overflow: „reject“:
try {
Temporal.PlainDate.from(
{ year: 2026, month: 2, day: 30 },
{ overflow: "reject" }
);
} catch (error) {
if (!(error instanceof RangeError)) throw error;
console.log("Please choose a valid calendar date.");
}
Без этого параметра объект { year: 2026, month: 2, day: 30 } будет ограничен 28 февраля. Строки в формате ISO ведут себя иначе: парсинг "2026-02-30" отклоняется полностью, вместо того чтобы быть скорректированным. Не стоит предполагать, что парсинг строк и создание объектов имеют одинаковое поведение при переполнении.
Допустимая дата всё равно может сочетаться с местным временем, которое никогда не наступает. Когда 8 марта 2026 года Нью-Йорк переводит часы вперед, время 2:30 ночи полностью пропускается. Параметр disambiguation определяет, что происходит с таким вводом:
try {
Temporal.ZonedDateTime.from(
"2026-03-08T02:30[America/New_York]",
{ disambiguation: "reject" }
);
} catch (error) {
if (!(error instanceof RangeError)) throw error;
console.log("Choose a time that exists and is unambiguous.");
}
При настройке disambiguation: "reject" класс Temporal выбрасывает исключение как в случае пропущенных времен, так и при повторяющихся временах, когда час воспроизводится дважды при сдвиге времени. Другие настройки автоматически выбирают подходящую интерпретацию. Для формы бронирования обычно проще попросить пользователя выбрать действительное время, чем изменять его встречу без уведомления.
7. Явное сравнение, измерение и сортировка дат
Для хронологического сравнения используйте встроенные методы типа. Начнем с двух дат:
const startDate = Temporal.PlainDate.from("2026-10-15");
const endDate = Temporal.PlainDate.from("2026-10-20");
Метод Temporal.PlainDate.compare() возвращает значение -1, 0 или 1 в зависимости от того, что первый аргумент раньше, равен или позже второго. Метод until() возвращает объект Temporal.Duration, который отображается в формате ISO 8601, поэтому пять дней обозначаются как P5D:
console.log(Temporal.PlainDate.compare(startDate, endDate));
// -1const difference = startDate.until(endDate, {
largestUnit: "days",
});console.log(difference.days); // 5
console.log(difference.toString()); // P5D
Для PlainDate разница измеряется в календарных днях. Он не рассчитывает пройденное время в часах в каком-либо часовом поясе, а также не определяет, учитывает ли ваш продукт день начала и день окончания; правило, связанное с ошибкой на единицу, должен определить пользователь.
Поскольку функция compare имеет сигнатуру, ожидаемую методом Array.prototype.sort, её можно использовать без дополнительных изменений. Для невыпорядоченного списка дат:
const dates = [
"2026-11-03",
"2026-10-15",
"2026-10-20",
].map(value => Temporal.PlainDate.from(value));
сортировка становится операцией в одну строку:
dates.sort(Temporal.PlainDate.compare);console.log(dates.map(date => date.toString()).join(", "));
// 2026-10-15, 2026-10-20, 2026-11-03
Избегайте привычек, унаследованных от класса Date. Вычитание объектов времени или их сравнение с помощью операторов < и > приводит к исключениям для этих типов, что сделано намеренно: это предотвращает случайные сравнения неправильного типа. Оператор === проверяет идентичность объектов, поэтому две одинаковые даты, созданные отдельно, не считаются равными по этому оператору; для проверки равенства значений используйте метод .equals(). Чтобы узнать о более тонких способах возвращения старых привычек, ознакомьтесь с нашим руководством о ловушках API времени, которые скрыто вновь вызывают проблемы с датами.
Выберите тип до выполнения вычислений
Большинство проблем с датами на самом деле являются проблемами моделирования, поэтому сначала определите, что означает данное значение:
- Дата без времени и часового пояса:
PlainDate - Время без даты и часового пояса:
PlainTime
PlainDateTimeInstantZonedDateTimeDurationПри разработке контракта API или столбца базы данных стоит обсудить этот вариант. Поле с простым названием date может вызвать разногласия относительно того, должно ли оно хранить календарную дату, временную метку UTC или локальное время.
Миграцию не обязательно проводить сразу. Выберите одну функциональность с четкими требованиями, определите формат входных данных, тип времени, правила работы с часовыми поясами и ожидаемый результат, а также добавьте тест-кейсы для конца месяцев и смены часовых поясов, если это применимо. Код, отвечающий за обработку дат, можно оставить без изменений до появления причин для его правки.
Чек-лист перед запуском в производство
- Проверьте поддержку временных данных во всех средах, куда вы выпускаете продукт, и если вы используете polyfill, учтите затраты на его загрузку и обработку в бюджете вашего пакета.
- Договоритесь с вашим бэкендом о формате сериализации. Обычная дата, момент времени в формате UTC и строка с идентификатором часового пояса в квадратных скобках — это три разных формата.
- Четко определите бизнес-правила: может ли фраза «через месяц» изменять дату, следует ли отклонять неоднозначные времена встреч и означает ли продолжительность календарные дни или прошедшие часы?
Заключение
Основной вклад Temporal заключается не в улучшении интерфейса API, а в выявлении различий, которые Date скрывал: даты против моментов времени, смещения против часовых поясов, календарная арифметика против прошедшего времени. Он предоставляет точные инструменты для каждого случая, включая строгие режимы, которые явно сообщают об ошибках вместо того, чтобы делать предположения. Однако он не может выбирать правила для вас. Вы сами должны тщательно определить эти правила, закодировать их в типах и параметрах, которые передаете, а также протестировать крайние случаи.