Спаўненне тыпоў часу з рэальнымі задачамі па датам: семь прыкладоў з адпаведнымі рашэннямі
Выпрацаваеце семь практычных задач з працэю з датамі та часамі за дапомогою JavaScript Temporal: ад простых дат і переплнення месяцаў да часовых зонаў, арыметыкі DST і пераканальнага контролю.
Крыніцы: JavaScript Temporal: 7 Date and Time Problems Explained Simply
Тэрміны, звязаныя з датамі, частаца здаюцца простымі: падлегчыць прыемку на месяц, паказаць зустрэч у часовым поясе колегі, післяльгаваць дні межы двумя датамі. Складнасці з’яўляюцца пазней, калі выявляецца, што люты мае меньш дзён, змяна часовага поясу прыводзіць да таго, што «завтра» аддаляецца на 23 гадзіны, або дзень нараджэння трэба зберагчы, нават як у ёму няма часовага пояса. API Temporal у JavaScript рашае гэтыя проблемы, даўш кожнам типу значэння свой сопственны тип, такім чынам сам тип паказвае, што означае даное значэння. Семь практычных прыкладаў нижэй паказваюць, який тип падходзіць для кожнай задачы, дзе стандартныя настройкі можу вас здзівіць, і якія рашэння Temporal намеравана залечыць вашай аплякацыі.
Настройкі месца для запуску прыкладаў
Натыўны падтрымкі ўсё яшчэ розвіваецца. На момент напісання MDN адзначае, што Temporal доступны толькі часткова, таму пераканайцеся ў падтрымцы ў вашых выбраных прыгледачах і версіі Node.js перш чым паспелагацца на вбудованую рэалізацыю. Старонка-справака Temporal у MDN стежыць за текущым статусам сумеснасці.
Ёжылкаваць без натыўнага падтрымкі можна, установіўшы 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 разлік выраховуецца у календарных дзёнach. Ён не выраховвае прыгнулі гадзіны ў жадным часовом поясе, і не выбірае, чы рахуецца ваш продукт і дзень пачатку, і дзень заканчэння; правіла, якія стосуюцца адлічэння дзён, вы самі должны задаць.
Пакалькі 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(). Чыба не ведаеце, якія ўскрытые способы можуць прывести да вярнення старых звычак, адзірніце нашу інструкцыю па падступам Temporal API, якія таямніча прыводзяць да багоў у роботе з датамі.
Выберыце тип перш чым пачнуце вырахунак
Большасць багоў, якія стосуюцца дат, на самай працоўцы ёсць багамі у моделюванні, таму спачатку ясна визначыце, што значыць данае значэнне:
- Дата без часу і часовага пояса:
PlainDate - Час без даты і часовага пояса:
PlainTime
PlainDateTimeInstantZonedDateTimeDurationКалі вы проектуецеяеяе кантракт API або столбець базы дадзеных, гэты выбор заставляе прыняць рашэнне. Поле пад назвай date выклікае неяснасці ў тым, чы гэта календарная дата, метка часу UTC чы местны час.
Міграцыю не трэба выканаць заўсёды адразу. Выберыце адну функцыю з чысткімі вакаліяцыямі, задаце яе формат уводу, тип Temporal, правілы часовай зоны і адказваны выход, а таксама дадзіце тэставыя кейсы для канца месацоў і змян гадзінню, калі гэта прыемліва. Код даты можна залишыць без змян, пакуль у вас не будзе прычыны яго змяніць.
Чакліст перад прыемлемасцю
- Пераканаўце падтрымку Temporal у кожнай средзе, да якой вы дастаўліяеце продукт, і якщо вы выкарыстоўваете polyfill, урахаваце косты яго завантажэння і аналізу ў вашым бюджете пакета.
- Дагаворыцеся пра формат серыявання дадзеных з вашым бэкендам. Звычная дата, момент у UTC і строка з ідэнтыфікатарам часовага поясу ў квадратных скобках — гэта тры разныя форматы.
- Чытка адзначыце бізнес-правілы: чы можа «адзін месяц пазней» змяніць дату, чы трэба адхіліваць неясныя часы запланавання, і чы трываласць мае значэнне календарных дзён чы прыйшлых гадзін?
Заключанне
Галоўны вклад Temporal — не проста болей прыемная аберонта API, а выявленне разліків, якіе Date затуманіў: даты протык узлёкаў, зсувы протык часовых зонаў, календарная арыметыка протык спялёгчанага часу. Ён дае вам точныя інструменты для кожнага случая, укладаючы ў сябе строгі режымы, якія чытка паведаюць пра адмову замест таго, каб рабіць заўважэння. Але ён не можа выбраць за вас правіла. Вы самі павінны рашыць гэтыя правіла, закодаваць іх у типы та параметры, якія перадаеце, і прабавіць усі краявыя случаўкі за дапамогою тэстаў.