Привет, друзья!
В этой статье я хочу рассказать вам о Temporal
, новом API
для работы с датой и временем в JS
.
Источником вдохновения для меня послужила эта замечательная статья.
Обратите внимание: предложение находится на 3 стадии рассмотрения и может подвергнуться некоторым изменениям, так что воздержитесь от его использования в продакшне до официального утверждения (вероятно, это произойдет где-то в конце текущего года).
Если вам это интересно, прошу под кат.
Сегодня у нас имеется 2 механизма для работы с датой и временем в JS
:
- объект
Date
- конструктор
Intl.DateTimeFormat
Недостатки Date
:
- манипуляции с датой/временем являются сложными;
- поддерживается только
UTC
и время на компьютере пользователя; - поддерживается только григорианский календарь;
- разбор строк в даты подвержен ошибкам;
- объекты
Date
являются мутабельными, т.е. изменяемыми, например:
const today = new Date()
const tomorrow = new Date(today.setDate(today.getDate() + 1))
console.log(tomorrow) // завтрашняя дата
console.log(today) // тоже завтрашняя дата!
Это и многое другое вынуждает разработчиков использовать библиотеки вроде moment.js
(поддержка прекращена) или ее современные альтернативы типа dayjs
или date-fns
при интенсивной работе с датой/временем.
Intl.DateTimeFormat
предназначен исключительно для чувствительного к языку (локали) форматирования даты и времени.
Подробнее о нем и, в целом, об объекте Intl
можно почитать здесь.
Temporal
Вот что можно сделать для того, чтобы поиграть с Temporal
:
- создаем шаблон
JS-проекта
с помощьюcreate-snowpack-app
:
# temporal-test - название проекта
# --template @snowpack/app-template-minimal - используемый шаблон
# --use-yarn - использовать yarn для установки зависимостей
# --no-git - не инициализировать Git-репозиторий
yarn create snowpack-app temporal-test --template @snowpack/app-template-minimal --use-yarn --no-git
# or
npx create-snowpack-app ...
# переходим в директорию
cd temporal-test
# открываем директорию в редакторе кода
# требуется предварительная настройка
code .
- устанавливаем полифил
@js-temporal/polyfill
yarn add @js-temporal/polyfill
# or
npm i @js-temporal/polyfill
- импортируем объекты и расширяем прототип
Date
вindex.js
:
import { Temporal, Intl, toTemporalInstant } from '@js-temporal/polyfill'
Date.prototype.toTemporalInstant = toTemporalInstant
Temporal
предоставляет следующие возможности.
Текущие дата и время
Объект Temporal.Now
возвращает текущие дату и время:
// время (UTC) с начала эпохи, т.е. с 00:00:00 1 января 1970 года
// в секундах
Temporal.Now.instant().epochSeconds
// в миллисекундах
Temporal.Now.instant().epochMilliseconds
// new Date().getTime()
// текущая временная зона
Temporal.Now.timeZone() // Asia/Yekaterinburg
// дата и время в текущей локации с учетом временной зоны
Temporal.Now.zonedDateTimeISO()
// 2022-01-06T13:39:44.178384177+05:00[Asia/Yekaterinburg]
// дата и время в другой временной зоне
Temporal.Now.zonedDateTimeISO('Europe/London')
// 2022-01-06T08:40:22.249422248+00:00[Europe/London]
// дата и время в текущей локации без учета временной зоны
Temporal.Now.plainDateTimeISO()
// 2022-01-06T13:52:19.54213954
// дата в текущей локации
Temporal.Now.plainDateISO()
// время в текущей локации
Temporal.Now.plainTimeISO()
"Мгновенные" дата и время
Объект Temporal.Instant
возвращает объект, представляющий фиксированную позицию во времени с точностью до наносекунд. Позиция (строка) форматируется согласно ISO 8601
следующим образом:
Temporal.Instant.from('2022-03-04T05:06+07:00')
// 2022-03-03T22:06:00Z
// 1 млрд. секунд с начала эпохи
Temporal.Instant.fromEpochSeconds(1.0e9)
// 2001-09-09T01:46:40Z
"Зонированные" дата и время
Объект Temporal.ZonedDateTime
возвращает объект, представляющий фиксированную позицию во времени с точностью до наносекунд с учетом временной зоны и календарной системы:
new Temporal.ZonedDateTime(
123456789000000000n, // наносекунды с начала эпохи (bigint)
Temporal.TimeZone.from('Asia/Yekaterinburg'), // временная зона
Temporal.Calendar.from('iso8601') // дефолтный календарь
) // 1973-11-30T02:33:09+05:00[Asia/Yekaterinburg]
Temporal.ZonedDateTime.from('2025-09-05T02:55:00+01:00[Europe/London]')
// 2025-09-05T02:55:00+01:00[Europe/London]
Temporal.ZonedDateTime.from({
timeZone: 'America/New_York',
year: 2025,
month: 2,
day: 28,
hour: 10,
minute: 15,
second: 0,
millisecond: 0,
microsecond: 0,
nanosecond: 0
}) // 2025-02-28T10:15:00-05:00[America/New_York]
"Обычные" дата и время
Обычные (plain) дата и время возвращают значения (год, месяц, день, час, минута, секунда и т.д.) без учета временной зоны:
-
Temporal.PlainDate
— дата:
// 2022-01-31
new Temporal.PlainDate(2022, 1, 31)
Temporal.PlainDate.from('2022-01-31')
-
Temporal.PlainTime
— время:
// 12:00:00
new Temporal.PlainTime(12, 0, 0)
Temporal.PlainTime.from('12:00:00')
-
Temporal.PlainDateTime
— дата и время:
// 2022-01-31T12:00:00
new Temporal.PlainDateTime(2022, 1, 31, 12, 0, 0)
Temporal.PlainDateTime.from('2022-01-31T12:00:00')
-
Temporal.PlainYearMonth
— месяц и год:
// июнь 2022 года
// 2022-06
new Temporal.PlainYearMonth(2022, 6)
Temporal.PlainYearMonth.from('2022-06')
-
Temporal.PlainMonthDay
— месяц и день:
// 4 мая
// 05-04
new Temporal.PlainMonthDay(5, 4)
Temporal.PlainMonthDay.from('05-04')
Значение даты и времени
Объект Temporal
содержит ряд полезных свойств/геттеров:
const date = Temporal.ZonedDateTime.from(
'2022-01-31T12:13:14+05:00[Asia/Yekaterinburg]'
)
date.year // 2022
date.month // 1
date.day // 31
date.hour // 12
date.minute // 13
date.second // 14
date.millisecond // 0
// и т.д.
Другие свойства:
-
dayOfWeek
— от1
для понедельника до7
для воскресенья; -
dayOfYear
— от1
до365
или366
в високосный год; -
weekOfYear
— от1
до52
или53
; -
daysInMonth
—28
,29
,30
или31
; -
daysInYear
—365
или366
; -
inLeapYear
—true
для високосного года.
Сравнение и сортировка даты и времени
Все объекты Temporal
содержат метод compare
, который возвращает:
-
0
, когдаdate1
иdate2
равны; -
1
, когдаdate1
"больше" (наступит или наступила позже), чемdate2
; -
-1
, когдаdate1
"меньше" (наступит или наступила раньше), чемdate2
.
const date1 = Temporal.Now.plainDateISO()
const date2 = Temporal.PlainDate.from('2022-04-05')
Temporal.PlainDateTime.compare(date1, date2) // -1
Разумеется, данный метод можно использовать для сортировки:
const sortedDates = [
'2022-01-01T00:00:00[Europe/London]',
'2022-01-01T00:00:00[Asia/Yekaterinburg]',
'2022-01-01T00:00:00[America/New_York]'
]
.map((d) => Temporal.ZonedDateTime.from(d))
.sort(Temporal.ZonedDateTime.compare)
console.log(sortedDates)
/*
[
'2022-01-01T00:00:00+05:00[Asia/Yekaterinburg]',
'2022-01-01T00:00:00+00:00[Europe/London]',
'2022-01-01T00:00:00-05:00[America/New_York]',
]
*/
Вычисление даты и времени
Все объекты Temporal
содержат методы add
, subtract
и round
для продолжительности (duration).
Продолжительность можно определить с помощью объекта Temporal.Duration
, передав ему years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds
, а также знак (sign) -1
для отрицательной и 1
для положительной продолжительности. Вместе с тем, стоит отметить, что указанные методы принимают любые подобные продолжительности (duration-like) значения без необходимости создания специального объекта.
// 2022-01-06
const today = new Temporal.PlainDate(2022, 1, 6)
// прибавляем 1 день
const tomorrow = today.add({ days: 1 })
// или
// прибавляем 2 дня
const dayAfterTomorrow = today.add(Temporal.Duration.from({ days: 2 }))
// вычитаем 1 день
const yesterday = today.subtract({ days: 1 })
console.log(tomorrow) // 2022-01-07
console.log(dayAfterTomorrow) // 2022-01-08
console.log(yesterday) // 2022-01-05
// сегодняшняя дата осталась неизменной
// объекты `Temporal` являются иммутабельными (неизменяемыми)
console.log(today) // 2022-01-06
const duration = Temporal.Duration.from({ days: 2, hours: 12 })
const durationInDays = duration.round({ smallestUnit: 'days' })
// 3 дня
// https://day.js.org/docs/en/durations/as-iso-string
console.log(durationInDays) // P3D
Предположим, что у нас имеется такой инпут:
<input type="date" class="calendar-input" />
Вот как можно установить "недельное" ограничение на выбор даты с помощью Temporal
и отформатировать вывод с помощью Intl.DateTimeFormat
:
const today = Temporal.Now.plainDateISO()
const afterWeek = today.add({ days: 7 })
const calendarInput = document.querySelector('.calendar-input')
calendarInput.min = today
calendarInput.max = afterWeek
calendarInput.value = today
const dateFormatter = new Intl.DateTimeFormat([], {
dateStyle: 'long'
})
calendarInput.onchange = ({ target: { value } }) => {
const date = Temporal.PlainDate.from(value)
const formattedDate = dateFormatter.format(date)
console.log(formattedDate) // например, 14 января 2022 г.
}
Методы until
и since
возвращают объект Temporal.Duration
, описывающий время до или после указанных даты и времени на основе текущей даты/времени:
// количество месяцев, оставшихся до d1
d1.until().months
// дней до d2
d2.until().days
// недель, прошедших с d3
d3.since().weeks
Метод equals
предназначен для определения идентичности (равенства) даты/времени:
const d1 = Temporal.PlainDate.from('2022-01-31')
const d2 = d1.add({ days: 1 }).subtract({ hours: 24 })
console.log(
d1.equals(d2)
) // true
console.log(
Temporal.PlainDate.compare(d1, d2)
) // 0
Строковые значения даты и времени
Все объекты Temporal
содержат метод toString
, который возвращает строковое представление даты/времени:
Temporal.Now.zonedDateTimeISO().toString()
// 2022-01-06T16:30:51.380651378+05:00[Asia/Yekaterinburg]
Temporal.Now.plainDateTimeISO().toString()
// 2022-01-06T16:32:47.870767866
Temporal.Now.plainDateISO().toString()
// 2022-01-06
Для форматирования даты/времени можно использовать объекты Intl
или Date
:
// объект `Temporal`, не строка
const d1 = Temporal.Now.plainDateISO()
// ok
new Intl.DateTimeFormat('ru-RU').format(d1) // 06.01.2022
new Intl.DateTimeFormat('en-US').format(d1) // 1/6/2022
new Intl.DateTimeFormat('de-DE').format(d1) // 6.1.2022
// не ok
new Date(d1).toLocaleDateString() // error
// строка
const d2 = Temporal.Now.plainDateISO().toString()
// ok
new Date(d2).toLocaleDateString() // 06.01.2022
// не ok
new Intl.DateTimeFormat().format(d2) // error
// но
new Intl.DateTimeFormat().format(new Date(d2)) // ok
Для преобразования объекта Date
в объект Temporal.Instant
предназначен метод toTemporalInstant
объекта Date
. Для обратного преобразования используется свойство epochMilliseconds
объектов Temporal.Instant
и Temporal.ZonedDateTime
:
// туда
const legacyDate1 = new Date()
const temporalInstant = legacyDate1.toTemporalInstant()
// сюда
const legacyDate2 = new Date(temporalInstant.epochMilliseconds)
Вместо заключения
Появление в JS
нового API
для работы с датой/временем — это, конечно, хорошо, но:
- интерфейс
Temporal
сложно назвать user friendly, в отличие отAPI
, предоставляемогоdayjs
и другими популярнымиJS-библиотеками
для работы с датой/временем; - излишне многословный синтаксис: например, зачем использовать
Temporal.Now.plainDateTimeISO(Temporal.Now.timeZone()).toString()
, который все равно придется форматировать, когда естьnew Date().toLocaleString()
; - отсутствие возможности форматирования даты/времени;
- для интеграции с
Date
иIntl
требуются дополнительные преобразования и т.д.
Вывод:
- если в приложении ведется или планируется активная работа с датой/временем, используйте библиотеку;
// yarn add dayjs
import dayjs from 'dayjs' // 6.8K!
// plugins
import isLeapYear from 'dayjs/plugin/isLeapYear'
import utc from 'dayjs/plugin/utc'
import dayOfYear from 'dayjs/plugin/dayOfYear'
import weekday from 'dayjs/plugin/weekday'
import isoWeeksInYear from 'dayjs/plugin/isoWeeksInYear'
import minMax from 'dayjs/plugin/minMax'
import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
import isBetween from 'dayjs/plugin/isBetween'
import advancedFormat from 'dayjs/plugin/advancedFormat'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import timezone from 'dayjs/plugin/timezone'
import updateLocale from 'dayjs/plugin/updateLocale'
import toArray from 'dayjs/plugin/toArray'
import toObject from 'dayjs/plugin/toObject'
import arraySupport from 'dayjs/plugin/arraySupport'
import objectSupport from 'dayjs/plugin/objectSupport'
// locale
import 'dayjs/locale/ru'
// plugins
dayjs.extend(isLeapYear)
dayjs.extend(utc)
dayjs.extend(weekday)
dayjs.extend(dayOfYear)
dayjs.extend(isoWeeksInYear)
dayjs.extend(minMax)
dayjs.extend(duration)
dayjs.extend(relativeTime)
dayjs.extend(isBetween)
dayjs.extend(advancedFormat)
dayjs.extend(localizedFormat)
dayjs.extend(timezone)
dayjs.extend(updateLocale)
// toArray()
dayjs.extend(toArray)
// toObject()
dayjs.extend(toObject)
// dayjs(array)
dayjs.extend(arraySupport)
// dayjs(object)
dayjs.extend(objectSupport)
// locale
// global
dayjs.locale('ru')
// instance
// dayjs().locale('ru').format()
dayjs.updateLocale('ru', {
ordinal: (n) => `${n}ое`
})
const localeDateTime1 = dayjs().format('DD.MM.YYYY, HH:mm:ss')
console.log(localeDateTime1) // 06.01.2022, 18:55:38
// native way
// new Date().toLocaleString()
/*
new Intl.DateTimeFormat([], {
dateStyle: 'short',
timeStyle: 'medium' // with seconds
}).format()
*/
const localeDateTime2 = dayjs().format('D MMMM YYYY г., HH:mm:ss')
console.log(localeDateTime2) // 6 января 2022 г., 19:01:01
// native way
/*
new Intl.DateTimeFormat([], {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format()
*/
// utc plugin
const utcDateTime = dayjs.utc().format('DD.MM.YYYY, HH:mm')
console.log(utcDateTime) // 06.01.2022, 14:11
// dayjs(date).isValid()
// weekday plugin
const dateFormat = 'DD.MM.YYYY'
const nextMonday = dayjs().weekday(7).format(dateFormat)
console.log(nextMonday) // 10.01.2022
// setters/getters
// millisecond, second, minute, hour,
// date, day (0 indexed), month (0 indexed), year
// and some special units
// or
// get('year') | get('y')
// set('date', 1) | set('D', 1)
// dayOfYear and isLeapYear plugins
const halfOfYearDate = dayjs()
.dayOfYear(dayjs().isLeapYear() ? 366 / 2 : 365 / 2)
.format(dateFormat)
console.log(halfOfYearDate) // 02.07.2022
// isoWeeksInYear plugin
// isLeapYear plugin is required
const weeksInThisYear = dayjs().isoWeeksInYear()
console.log(weeksInThisYear) // 52
// minMax plugin
// max(date1, date2, ...dateN) | max(date[])
const maxDate = dayjs
.max(dayjs(), dayjs('2021-12-31'), dayjs('2022-05-03'))
.format(dateFormat)
console.log(maxDate) // 03.05.2022
// calculations
// subtract(value: number, unit?: string) | subtract({ unit: value })
const today = dayjs()
const yesterday = today.subtract(1, 'day').format(dateFormat)
// or using duration
// duration plugin is required
// duration(value: number, unit?: string) | duration({ unit: value })
const anotherYesterday = today
.subtract(dayjs.duration({ day: 1 }))
.format(dateFormat)
// native way
/*
const today = new Date()
const yesterday = new Date(
today.setDate(today.getDate() - 1)
).toLocaleDateString()
*/
const dayAfterTomorrow = today.add(2, 'days').format(dateFormat)
console.log(yesterday) // 05.01.2022
console.log(dayAfterTomorrow) // 08.01.2022
const lastMonday = dayjs().startOf('week').format(dateFormat)
console.log(lastMonday) // 03.01.2022
const lastDayOfCurrentMonth = dayjs().endOf('month').format('dddd')
console.log(lastDayOfCurrentMonth) // понедельник
const timeFormat = 'HH:mm'
// get UTC offset in minutes
// convert minutes to hours
const myUtcOffset = dayjs().utcOffset() / 60
console.log(myUtcOffset) // 5
// set UTC offset in hours (from -16 to 16) or minutes
const localTimeFromUtc = dayjs.utc().utcOffset(myUtcOffset).format(timeFormat)
console.log(localTimeFromUtc) // 19:55
// relativeTime plugin
const d1 = '1380-09-08'
// fromNow(withoutSuffix?: boolean)
// from(date, withoutSuffix?)
const fromNow = dayjs(d1).fromNow()
console.log(fromNow) // 641 год назад
const d2 = '2022-07-02'
// toNow(withoutSuffix?: boolean)
// to(date, withoutSuffix?)
const toDate = dayjs().to(d2)
console.log(toDate) // через 6 месяцев
// difference
const d3 = dayjs('2021-07-01')
const d4 = dayjs('2022-01-01')
// date1.diff(date2, unit?: string, withoutTruncate?: boolean)
const diffInMonths = d4.diff(d3, 'month')
console.log(diffInMonths) // 6
// unix() - in seconds
const unixTimestampInMs = dayjs(d3).valueOf()
console.log(unixTimestampInMs) // 1625079600000
const daysInCurrentMonth = dayjs().daysInMonth()
console.log(daysInCurrentMonth) // 31
// toJSON()
const currentDateInIso = dayjs().toISOString()
console.log(currentDateInIso) // 2022-01-07T12:49:51.238Z
// query
// default unit is ms
// isBefore(date, unit?: string)
// isSame(date, unit?)
// isAfter(date, unit?)
// isSameOrBefore | isSameOrAfter plugins are required
// isSameOrBefore(date, unit?) | isSameOrAfter(date, unit?)
// isBetween plugin is required
// [ - inclusion of date, ( - exclusion
// isBetween(date1, date2, unit?: string, inclusivity?: string)
const isTodayStillWinter = dayjs().isBetween(
'2021-12-01',
'2022-03-01',
'day',
'[)'
)
console.log(isTodayStillWinter) // true
const myTimezone = dayjs.tz.guess()
console.log(myTimezone) // Asia/Yekaterinburg
const currentDateTimeAdvanced = dayjs().format(
'Do MMMM YYYY г., HH часов mm минут (z)'
)
console.log(currentDateTimeAdvanced) // 7ое январь 2022 г., 18 часов 13 минут (GMT+5)
const currentDateTimeLocalized = dayjs().format('LLLL (zzz)')
console.log(currentDateTimeLocalized)
// пятница, 7 января 2022 г., 18:44 (Yekaterinburg Standard Time)
// humanize(withSuffix?: boolean)
const inADay = dayjs.duration(1, 'day').humanize(true)
console.log(inADay) // через день
- если манипуляции с датой/временем простые, немногочисленные и не предполагают разного формата вывода, используйте
Temporal
+Date
илиIntl.DateTimeFormat
.
Пожалуй, это все, чем я хотел поделиться с вами в данной статье.
Благодарю за внимание и happy coding!
Комментарии (10)
Bigata
12.01.2022 16:47Так это сторонняя библиотека как и другие, да?
Shannon
13.01.2022 09:38Это предложение нового API, которое может стать стандартом JS, если пройдет все этапы. Сейчас оно находится на стадии 3, поэтому вероятность того, что оно скоро появится как стандарт достаточно велико.
Bigata
13.01.2022 09:52А вот это интересно. Правде не всё в стандарт включается из экспериментальных технологий, но будем надеяться.
akazakou
13.01.2022 12:12Хз. Большую часть времени было достаточно использовать нативный Date. Во фронте был проект где надо было активно работать с отображением дат в разных часовых поясах. Там отдично зашел Luxon - наследство moment.js
mayorovp
14.01.2022 00:36Нативный Date — та ещё гадость. Одно то что оно только в местной таймзоне работает уже источник неудобств. А внутренняя мутабельность — источник ошибок что в Реакте, что в Ангуляре.
Вот Luxon уже лучше, но в чужих компонентах обычно не используется. А потому приходится постоянно из него в Date гонять и обратно.
RealPeha
По-моему стало только хуже. Может и решили какие-то проблемы
Date
, но в целом лучше не стало. Придется и дальше сторонние библиотеки использоватьmayorovp
Надеюсь, под "сторонними библиотеками" вы не moment.js понимаете?
RealPeha
А даже если и его? Я про сам факт того, что библиотеки все равно продолжат жить
mayorovp
Кстати про "лучше не стало". Когда предложение примут — появится как минимум "общий язык" для библиотек и компонентов, которые работают с датами.