Підбір типів часу до реальних завдань з датами: сім прикладів із поясненнями
Виконайте сім повсякденних завдань з роботи з датами та часом за допомогою 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. Віднімання об’єктів типу Temporal чи їх порівняння з символами < та > призводить до виникнення помилок для цих типів, що є навмисним: це запобігає випадковому порівнянню неправильного типу. А оператор === перевіряє ідентичність об’єктів, тому дві однакові дати, створені окремо, не є ідентичними за допомогою ===; для перевірки рівності значень використовуйте .equals(). Щоб дізнатися про більш тонкі способи, якими старі звички можуть проявитися знову, ознайомтесь із нашим посібником „Пастки API Temporal, які знову вводять помилки з датами“.
Виберіть тип перед виконанням обчислень
Більшість помилок, пов’язаних із датами, насправді є помилками у моделюванні, тому спочатку слід чітко визначити, що означає певне значення:
- Дата без часу та часової зони:
PlainDate - Час без дати та часової зони:
PlainTime
PlainDateTimeInstantZonedDateTimeDurationЦей вибір потребує обговорення під час розробки контракту API або стовпця бази даних. Поле під назвою просто date спричиняє розбіжності щодо того, чи має воно календарну дату, часовий мітку UTC чи локальний час.
Міграцію не обов’язково робити одразу. Виберіть одну функцію з чіткими вимогами, визначте її формат вхідних даних, тип Temporal, політику часових зон та очікуваний вихід, а також додайте тестові випадки для кінця місяців та змін годинникових поясів, де це застосовується. Код для роботи з датами можна залишити як є, поки у вас не з’явиться причина його змінювати.
Чек-лист перед випуском
- Перевірте підтримку функцій, пов’язаних із часом, у кожному середовищі, до якого ви розповсюджуєте продукт, і якщо ви використовуєте polyfill, врахуйте витрати на його завантаження та обробку у бюджеті вашого пакету.
- Домовтесь щодо формату серіалізації даних із вашим бекендом. Звичайна дата, момент у форматі UTC та рядок із ідентифікатором часового поясу в дужках — це три різні формати.
- Чітко визначте бізнес-правила: чи може фраза „через місяць“ змінювати день, чи слід відхиляти неоднозначні часи зустрічей, та чи означає тривалість календарні дні чи минулі години?
Підсумок
Основний внесок Temporal полягає не у покращеній інтерфейсній моделі API, а у примусовому розрізненні тих аспектів, які були розмиті типом Date: дати проти моментів часу, зсуви проти часових поясів, календарна арифметика проти фактично минулого часу. Він надає точні інструменти для кожного випадку, включаючи суворі режими, які чітко повідомляють про помилки замість того, щоб припускати їх наявність. Однак він не може обирати правила для вас. Ви самі маєте свідомо визначити ці правила, закодувати їх у типах та параметрах, які передаєте, та протестувати крайні випадки.