Привет, друзья!


В этой статье я хочу рассказать вам о Temporal, новом API для работы с датой и временем в JS.



Источником вдохновения для меня послужила эта замечательная статья.


Обратите внимание: предложение находится на 3 стадии рассмотрения и может подвергнуться некоторым изменениям, так что воздержитесь от его использования в продакшне до официального утверждения (вероятно, это произойдет где-то в конце текущего года).


Если вам это интересно, прошу под кат.


Сегодня у нас имеется 2 механизма для работы с датой и временем в JS:



Недостатки 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:



# 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 .


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) дата и время возвращают значения (год, месяц, день, час, минута, секунда и т.д.) без учета временной зоны:



// 2022-01-31
new Temporal.PlainDate(2022, 1, 31)
Temporal.PlainDate.from('2022-01-31')


// 12:00:00
new Temporal.PlainTime(12, 0, 0)
Temporal.PlainTime.from('12:00:00')


// 2022-01-31T12:00:00
new Temporal.PlainDateTime(2022, 1, 31, 12, 0, 0)
Temporal.PlainDateTime.from('2022-01-31T12:00:00')


// июнь 2022 года
// 2022-06
new Temporal.PlainYearMonth(2022, 6)
Temporal.PlainYearMonth.from('2022-06')


// 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;
  • daysInMonth28, 29, 30 или 31;
  • daysInYear365 или 366;
  • inLeapYeartrue для високосного года.

Сравнение и сортировка даты и времени


Все объекты 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 требуются дополнительные преобразования и т.д.

Вывод:


  • если в приложении ведется или планируется активная работа с датой/временем, используйте библиотеку;

Шпаргалка по dayjs:
// 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)


  1. RealPeha
    12.01.2022 11:02

    По-моему стало только хуже. Может и решили какие-то проблемы Date, но в целом лучше не стало. Придется и дальше сторонние библиотеки использовать


    1. mayorovp
      12.01.2022 11:07

      Надеюсь, под "сторонними библиотеками" вы не moment.js понимаете?


      1. RealPeha
        12.01.2022 11:12
        +1

        А даже если и его? Я про сам факт того, что библиотеки все равно продолжат жить


    1. mayorovp
      12.01.2022 11:25

      Кстати про "лучше не стало". Когда предложение примут — появится как минимум "общий язык" для библиотек и компонентов, которые работают с датами.


  1. aleks_raiden
    12.01.2022 12:10

    Из интересного это доступность наносекундного разрешения - это гуд


  1. Bigata
    12.01.2022 16:47

    Так это сторонняя библиотека как и другие, да?


    1. Shannon
      13.01.2022 09:38

      Это предложение нового API, которое может стать стандартом JS, если пройдет все этапы. Сейчас оно находится на стадии 3, поэтому вероятность того, что оно скоро появится как стандарт достаточно велико.


      1. Bigata
        13.01.2022 09:52

        А вот это интересно. Правде не всё в стандарт включается из экспериментальных технологий, но будем надеяться.


  1. akazakou
    13.01.2022 12:12

    Хз. Большую часть времени было достаточно использовать нативный Date. Во фронте был проект где надо было активно работать с отображением дат в разных часовых поясах. Там отдично зашел Luxon - наследство moment.js


    1. mayorovp
      14.01.2022 00:36

      Нативный Date — та ещё гадость. Одно то что оно только в местной таймзоне работает уже источник неудобств. А внутренняя мутабельность — источник ошибок что в Реакте, что в Ангуляре.


      Вот Luxon уже лучше, но в чужих компонентах обычно не используется. А потому приходится постоянно из него в Date гонять и обратно.