Пошук уроків, статей та іншого контенту
Розберете надійні підходи до зберігання UTC, передачі ISO-значень і відображення часу в часовій зоні користувача.
Надійна система повинна розрізняти щонайменше три різні поняття:
Момент часу — конкретна точка на часовій шкалі.
Локальна дата й час — календарне значення без однозначного часового поясу.
Правила часового поясу — наприклад, Europe/Kyiv, які визначають зміщення UTC для конкретної дати, включно з переходами на літній і зимовий час.
Наприклад, значення 2026-03-29 02:30 саме по собі не визначає момент часу. Для нього потрібен часовий пояс. Ба більше, у деяких часових поясах така локальна година може не існувати через перехід на літній час.
Тому базове правило таке:
Момент часу зберігайте як UTC, передавайте як однозначне ISO-значення, а відображайте в часовій зоні користувача.
Це правило не стосується всіх можливих дат. Для дня народження, святкового дня або розкладу «щодня о 09:00» потрібна інша модель.
Момент часу — це подія на глобальній часовій шкалі:
час створення замовлення;
час відправлення повідомлення;
час завершення платежу;
час оновлення запису.
Такий момент однаковий для всіх користувачів. Відрізняється лише його представлення.
Наприклад:
2026-07-26T12:00:00.000ZЦе той самий момент, що й:
2026-07-26T15:00:00+03:00Суфікс Z означає UTC. Значення +03:00 — явне зміщення від UTC.
Локальна дата й час не є моментом, поки не задано часовий пояс:
2026-07-26 15:00Це може означати різні моменти:
2026-07-26T15:00:00+03:00;
2026-07-26T15:00:00-04:00;
2026-07-26T15:00:00Z.
Не можна передавати таке значення через API для події, якщо контракт очікує однозначний момент.
Дата без часу:
2026-07-26Приклади:
дата народження;
дата оплати за рахунком;
дата початку відпустки;
державне свято.
Перетворювати таку дату в UTC без бізнес-вимоги небезпечно. Якщо зберігати 2026-07-26 як опівніч UTC, у часовій зоні з від’ємним зміщенням користувач може побачити попередній день.
Значення «щодня о 09:00 за київським часом» — це не один момент. Це правило, яке породжує різні моменти в різні дні.
Для такого правила зазвичай потрібні окремі поля:
time: "09:00"
timeZone: "Europe/Kyiv"Не слід одразу замінювати його на UTC, якщо розклад має залишатися о 09:00 за місцевим часом після переходу на літній або зимовий час.
DateУ JavaScript Date представляє момент часу як кількість мілісекунд від Unix epoch:
1970-01-01T00:00:00.000ZУсередині Date не зберігає назву часового поясу користувача. Часовий пояс використовується лише під час форматування локального представлення.
const instant = new Date("2026-07-26T12:00:00.000Z");
console.log(instant.getTime());
// Кількість мілісекунд від Unix epoch
console.log(instant.toISOString());
// 2026-07-26T12:00:00.000Z
console.log(instant.toString());
// Представлення в часовій зоні середовища виконанняМетоди без суфікса UTC, наприклад getHours(), використовують локальну часову зону середовища:
instant.getHours();
instant.getUTCHours();Ці значення можуть бути різними.
Date працює з мілісекундами:
const value = new Date("2026-07-26T12:00:00.123Z");
console.log(value.getMilliseconds());
// 123Якщо домен потребує мікросекунд або наносекунд, стандартний Date не зберігає таку точність. Точність потрібно явно визначити в контракті системи та на рівні бази даних.
Для моментів часу використовуйте повне ISO-значення із часовою інформацією:
2026-07-26T12:00:00.000ZДопустимим також є значення з явним зміщенням:
2026-07-26T15:00:00+03:00Проте всередині системи часто зручно нормалізувати всі моменти до UTC.
{
"id": "order-123",
"createdAt": "2026-07-26T12:00:00.000Z",
"paidAt": null
}Для значень із часовою зоною:
{
"startsAt": "2026-07-26T12:00:00.000Z",
"timeZone": "Europe/Kyiv"
}Якщо клієнт отримує момент без Z або без зміщення, він не може надійно визначити його значення.
Обережно з таким форматом:
2026-07-26T12:00:00Це локальна дата й час без зміщення. Її інтерпретація залежить від контексту та середовища виконання. Для глобальної події такий формат краще заборонити на рівні схеми API.
Зберігайте:
тип, який підтримує часову семантику, якщо це можливо;
або UTC-значення з чітко визначеною точністю;
або число мілісекунд/мікросекунд від epoch, якщо це є стандартом вашої системи.
Вибір типу залежить від конкретної СУБД, але важливішою за назву типу є семантика:
чи зберігається саме момент;
чи зберігається зміщення;
чи нормалізується значення до UTC;
яка точність підтримується.
Зберігайте окремо дату:
2026-07-26Не перетворюйте її на Date, якщо час не має бізнесового значення.
Зберігайте щонайменше:
localTime: "09:00"
timeZone: "Europe/Kyiv"Для складних правил можуть знадобитися:
частота повторення;
день тижня;
дата початку;
дата завершення;
політика для неоднозначного часу під час переходу на літній час.
Часова зона і зміщення — не одне й те саме.
+03:00є зміщенням для конкретного моменту.
Europe/Kyivє ідентифікатором набору правил, які можуть змінюватися залежно від дати.
Якщо потрібно відновити майбутній розклад, зберігайте назву часової зони IANA, а не лише +03:00.
Якщо браузер створює момент часу:
const payload = {
occurredAt: new Date().toISOString()
};
console.log(payload);
// { occurredAt: "2026-07-26T...000Z" }toISOString() повертає UTC-значення з суфіксом Z.
Для користувацького розкладу краще передавати локальні компоненти та часову зону окремо:
const payload = {
localDate: "2026-07-26",
localTime: "09:00",
timeZone: "Europe/Kyiv"
};Сервер має інтерпретувати це значення за правилами вказаної часової зони.
Не варто покладатися лише на часову зону браузера. Користувач може:
подорожувати;
працювати віддалено;
мати часову зону профілю, відмінну від системної;
створювати подію для іншого регіону.
Тому часову зону для бізнесових операцій бажано зберігати в профілі або явно вибирати в інтерфейсі.
Для форматування моменту використовуйте Intl.DateTimeFormat, а не ручне додавання або віднімання годин.
const instant = new Date("2026-07-26T12:00:00.000Z");
const formatter = new Intl.DateTimeFormat("uk-UA", {
dateStyle: "medium",
timeStyle: "short",
timeZone: "Europe/Kyiv"
});
console.log(formatter.format(instant));Часова зона може бути визначена середовищем:
const userTimeZone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
const formatter = new Intl.DateTimeFormat("uk-UA", {
dateStyle: "long",
timeStyle: "short",
timeZone: userTimeZone
});
console.log(formatter.format(new Date()));Однак resolvedOptions().timeZone — це часова зона середовища браузера, а не обов’язково бізнесова часова зона користувача. Якщо користувач має налаштування в профілі, використовуйте саме його.
function parseInstant(value) {
if (typeof value !== "string") {
throw new TypeError("Момент часу має бути рядком");
}
const instant = new Date(value);
if (Number.isNaN(instant.getTime())) {
throw new RangeError("Некоректний ISO-рядок");
}
return instant;
}
function formatForUser(value, timeZone, locale = "uk-UA") {
const instant = parseInstant(value);
return new Intl.DateTimeFormat(locale, {
dateStyle: "medium",
timeStyle: "long",
timeZone
}).format(instant);
}
const apiValue = "2026-07-26T12:00:00.000Z";
console.log(formatForUser(apiValue, "Europe/Kyiv"));
console.log(formatForUser(apiValue, "America/New_York"));Цей приклад форматуватиме один і той самий момент по-різному, але сам момент не змінюється.
Регулярний вираз може перевірити форму рядка, але не завжди його календарну коректність.
Наприклад, форма може бути правильною:
2026-02-31T12:00:00.000Zале дата не існує.
Для зовнішніх даних потрібно:
перевірити тип;
перевірити формат;
перевірити, що значення справді розпізнається;
нормалізувати його;
відхилити неоднозначні формати.
function parseUtcInstant(value) {
if (typeof value !== "string") {
throw new TypeError("Очікується рядок");
}
// Для моменту вимагаємо явну UTC-зону.
if (!value.endsWith("Z")) {
throw new RangeError("Момент має закінчуватися на Z");
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
throw new RangeError("Некоректний момент часу");
}
return date;
}
const valid = parseUtcInstant("2026-07-26T12:00:00.000Z");
console.log(valid.toISOString());У виробничому коді формат контракту має бути узгоджений із сервером. Якщо API дозволяє зміщення на кшталт +03:00, перевірка повинна підтримувати й цей формат.
Переходи між стандартним і літнім часом створюють дві проблеми.
Під час переведення годинника вперед локальний інтервал може бути пропущений. Наприклад, після 01:59:59 одразу настає 03:00:00.
Розклад на 02:30 у таку дату не має однозначного результату:
пропустити виконання;
перенести на 03:00;
виконати в інший момент;
вважати розклад некоректним.
Це має бути бізнесове рішення.
Під час переведення годинника назад одна й та сама локальна година може настати двічі. Значення:
01:30може відповідати двом різним моментам.
Саме тому локальний час без часової зони та політики розв’язання не можна безпечно перетворити на UTC.
Для кожного локального розкладу визначте політику:
reject — відхилити неіснуючий або неоднозначний час;
shiftForward — перенести неіснуючий час уперед;
earlier — вибрати перше входження повторюваного часу;
later — вибрати друге входження.
Таку політику потрібно зафіксувати в доменному контракті, а не залишати на розсуд різних клієнтів.
Не плутайте час створення події з часом її виконання.
Наприклад, нагадування може містити:
{
"schedule": {
"localTime": "09:00",
"timeZone": "Europe/Kyiv"
},
"nextRunAt": "2026-07-27T06:00:00.000Z"
}localTime і timeZone описують правило, а nextRunAt — конкретний обчислений момент.
Після виконання системі потрібно:
обчислити наступну локальну дату;
застосувати правила часової зони;
обробити неіснуючий або повторюваний час;
отримати наступний UTC-момент;
зберегти його для планувальника.
Перевага такого підходу — планувальник працює з однозначними UTC-моментами, а доменне правило не втрачає локальну семантику.
Календарний час підходить для журналів і бізнесових подій, але не завжди для вимірювання тривалості.
Системний годинник може змінитися через:
синхронізацію NTP;
ручну зміну часу;
перехід на літній час;
віртуалізацію;
корекцію годинника операційною системою.
Для вимірювання тривалості в браузері використовуйте монотонний годинник:
const startedAt = performance.now();
// Виконання операції
for (let index = 0; index < 1_000_000; index += 1) {
Math.sqrt(index);
}
const elapsedMilliseconds = performance.now() - startedAt;
console.log(`Операція тривала приблизно ${elapsedMilliseconds} мс`);performance.now() не слід зберігати як час події або передавати на сервер. Це відносне значення для вимірювання тривалості в межах одного середовища.
На сервері аналогічно використовуйте монотонний таймер, якщо він доступний у вашому середовищі виконання.
У розподілених системах час на різних серверах може відрізнятися. Тому:
не використовуйте час клієнта як безумовно достовірний;
для аудиту використовуйте час сервера;
синхронізуйте системні годинники серверів;
не покладайтеся лише на createdAt для визначення порядку подій;
для строгого порядку використовуйте sequence number, версію або ідентифікатор події.
Наприклад, два сервери можуть створити події з такими часовими мітками:
2026-07-26T12:00:00.100Z
2026-07-26T12:00:00.090ZЦе не обов’язково означає, що друга подія відбулася раніше в логічному сенсі. Для причинно-наслідкового порядку потрібен окремий механізм.
Для відображення події корисно показувати користувачу:
дату й час у його часовій зоні;
за потреби назву або скорочення часової зони;
оригінальну часову зону події, якщо вона має значення.
Наприклад, міжнародна конференція може бути показана як:
26 липня, 15:00 за КиєвомА для локальної події користувача достатньо його локального часу.
Не додавайте часову зону до кожного значення без потреби. Надмірна технічна інформація погіршує сприйняття, але для перельотів, дедлайнів і міжнародних зустрічей зона критично важлива.
Для значень формату YYYY-MM-DD не використовуйте бездумно:
new Date("2026-07-26");Таке значення може бути інтерпретоване як UTC, після чого локальне форматування покаже інший календарний день у часовій зоні з від’ємним зміщенням.
Якщо потрібна саме календарна дата, зберігайте її як рядок або як спеціалізоване календарне значення:
const birthDate = "1990-07-26";
const [year, month, day] = birthDate.split("-").map(Number);
console.log({ year, month, day });У такому коді дата не перетворюється на момент часу і не залежить від часового поясу.
Temporal та спеціалізовані типиДля складних сценаріїв модель Date недостатньо виразна. Потрібні окремі типи для:
моменту часу;
календарної дати;
локальної дати й часу;
часової зони;
тривалості.
У середовищах, де доступний Temporal, його модель краще передає ці відмінності. Наприклад, концептуально:
Temporal.Instant — момент UTC;
Temporal.PlainDate — календарна дата без часового поясу;
Temporal.PlainTime — локальний час;
Temporal.ZonedDateTime — локальна дата, час і часова зона разом.
Перед використанням потрібно перевірити підтримку конкретним браузером або середовищем виконання. Якщо підтримка відсутня, використовуйте сумісне рішення, узгоджене з інфраструктурою проєкту, і не змішуйте його типи з Date без явних перетворень.
Головна перевага такого підходу — типи допомагають не змішувати календарну дату з моментом часу.
Для кожного поля зафіксуйте його семантику.
Наприклад:
{
"createdAt": "2026-07-26T12:00:00.000Z",
"dueDate": "2026-07-30",
"meeting": {
"startsAt": "2026-07-26T12:00:00.000Z",
"timeZone": "Europe/Kyiv"
},
"dailyReminder": {
"localTime": "09:00",
"timeZone": "Europe/Kyiv"
}
}Тут:
createdAt — момент;
dueDate — календарна дата;
meeting.startsAt — конкретний момент;
meeting.timeZone — зона, у якій подію створено або яку потрібно показати;
dailyReminder — локальне правило повторення.
У документації API також вкажіть:
чи дозволене зміщення замість Z;
максимальну точність;
чи приймаються секунди та мілісекунди;
як обробляються неіснуючі локальні часи;
яка часова зона використовується за замовчуванням;
чи сервер нормалізує значення до UTC.
Тести мають охоплювати не лише звичайні дати.
Перевіряйте:
перехід року;
високосний рік;
кінець місяця;
переходи на літній і зимовий час;
різні часові зони;
позитивні та негативні зміщення;
дати до і після Unix epoch;
значення з мілісекундами;
некоректні ISO-рядки;
локальні часи, яких не існує;
повторювані локальні часи;
поведінку під час зміни часового поясу пристрою.
Функції форматування краще тестувати з явно переданою часовою зоною:
function formatInstant(value, timeZone) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
throw new Error("Некоректний момент часу");
}
return new Intl.DateTimeFormat("uk-UA", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
timeZone
}).format(date);
}
const value = "2026-07-26T12:00:00.000Z";
console.log(formatInstant(value, "UTC"));
console.log(formatInstant(value, "Europe/Kyiv"));
console.log(formatInstant(value, "America/New_York"));Явна зона робить тест детермінованим незалежно від налаштувань машини, на якій він запускається.
Значення 2026-07-26 15:00 не можна називати UTC без доказу, що саме це мав на увазі користувач.
Z або зміщення в APIРядок без часової зони не є однозначним моментом. Контракт має вимагати Z або явне зміщення.
Код на кшталт:
const localTime = new Date(
utcTime.getTime() + 3 * 60 * 60 * 1000
);ламається під час переходів на літній час і для інших часових зон. Для форматування використовуйте Intl.DateTimeFormat.
+02:00 не замінює Europe/Kyiv. Зміщення описує конкретний момент, а часова зона — правила календаря.
DateДата народження або дата рахунку не стає моментом лише через створення об’єкта Date. Таке перетворення може змінити день під час відображення.
Користувач може мати неправильний годинник або змінити його. Для серверного аудиту джерелом часу має бути сервер.
ISO-рядки можна безпечно порівнювати лексикографічно лише за однакового формату й однакової нормалізації. Рядки з різними зміщеннями краще спочатку перетворити на моменти.
Date.now()Date.now() показує системний календарний час, який може змінитися. Для тривалості використовуйте монотонний таймер.
Автоматичне перетворення неіснуючої дати на іншу дату може призвести до непомітних помилок у платежах, нагадуваннях і дедлайнах. Краще явно відхилити значення або застосувати заздалегідь визначену політику.
Для кожного часового поля поставте такі запитання:
Це момент, календарна дата, локальний час чи правило повторення?
Чи має значення часова зона?
Чи потрібно зберігати оригінальну зону користувача?
Яка потрібна точність?
Чи може значення бути майбутнім розкладом?
Що відбувається під час переходу на літній час?
Хто є джерелом істини для часу — клієнт чи сервер?
Як значення буде валідовано на межі системи?
У якому форматі воно передається через API?
Як воно відображається в різних часових зонах?
Якщо відповіді на ці запитання відсутні, часове поле ще не має завершеної моделі.
Момент часу зберігайте й передавайте як однозначне UTC-значення.
Для API використовуйте ISO-рядки з Z або явним зміщенням.
Локальну календарну дату не перетворюйте на Date без потреби.
Для розкладів зберігайте локальний час і IANA-ідентифікатор часової зони.
Не плутайте часову зону зі зміщенням.
Форматуйте час через Intl.DateTimeFormat, а не ручним додаванням годин.
Для вимірювання тривалості використовуйте монотонний годинник.
Обробку переходів на літній час визначайте як частину бізнес-логіки.
Тестуйте систему в різних часових зонах і на межах календаря.
Семантика часового поля важливіша за конкретний формат його зберігання.