Вы когда-нибудь чувствовали, что дата Javascript 0.1.2023, представляющая Новый год, не имеет никакого смысла? Если это так, давайте посмотрим, как будущий Javascript исправит это, а также пару других проблем с DateJS.
Но обо всем по порядку: почему январь равен нулю? Ответ лежит в далеком прошлом. У автора Javascript Брендана Эйха не хватило времени, чтобы изобрести собственный формат даты Javascript. Таким образом, он скопировал дату из Java, в которой месяцы начинались с нуля. Тогда почему Java представила январь как нулевой месяц? Потому что такое же поведение было в языке POSIX C. Почему POSIX C имеет такое поведение? Давайте остановимся здесь — продолжение будет просто провалом в кроличью нору.
К сожалению, причудливая нумерация месяцев — это лишь верхушка айсберга. Проблемы с JS Date многочисленны. Назвать несколько:
- Вы не можете работать с датой без части времени
- JS Date поддерживает только часовой пояс пользователя и UTC.
- Конструктор JS Date не проверяет границы
new Date(2021,1,31).toDateString() equals Wed Mar 03 2021
Кроме того, изменчивость JS Date — еще одна большая проблема. Посмотрите на следующий код и обратите внимание, что завтра равно сегодня:
const today = new Date() | |
const tomorrow = today.setDate(today.getDate() + 1) | |
tomorrow.valueOf() === today.valueOf() // => true |
Это определенно не дружественное к программисту поведение.
Работать с датой и временем сложно
Помимо всех вышеупомянутых проблем JS Date (также известных как случайная сложность), измерение времени и даты само по себе содержит огромную внутреннюю сложность.
Мне всегда было интересно, почему работа с датами кажется более сложной, чем работа со строками, числами или логическими значениями. Во многих языках тип date
является просто примитивным типом, таким как типы string
или number
. Однако, в отличие от чисел, у меня всегда были трудности (каламбур) с арифметикой даты и времени, независимо от того, какой язык я использую.
Мое подозрение, что с датами что-то не так, подтвердилось знаменитым Видео Computerphile о часовых поясах. Поэтому, если кто-то считает, что даты — это несколько более простая тема, посмотрите его объяснение. Вы увидите, о чем я говорю :).
MomentJS, Luxon, дата-fns
Чтобы смягчить упомянутые выше проблемы, сообщество JS начало создавать новые библиотеки. Вероятно, наиболее известным был MomentJS, удобный инструмент, устраняющий множественные сбои в типе JS Date. MomentJS — отличная библиотека, но сегодня я бы рекомендовал использовать Luxon. Luxon, пришедший на смену MomentJS, предлагает неизменяемость и нативные операторы. Неизменяемость Luxon особенно полезна, как вы можете видеть в следующем примере:
// MomentJS | |
var m1 = moment() | |
var m2 = m1.add(1, 'minutes') | |
m1.valueOf() === m2.valueOf() // ==> true | |
// Luxon | |
var now = DateTime.now() | |
var yesterday = now.plus({ days: -1 }) | |
now === yesterday // ==> false |
Другая популярная библиотека, Date-fns, предоставляет набор служебных функций, пытающихся исправить основные болевые точки стандартного объекта JS Date. Тем не менее, вы по-прежнему работаете с JS Dates, а это значит, что даже в официальном примере есть те же странные вибрации JS Date:
import { format } from 'date-fns' | |
format(new Date(2014, 1, 11), 'yyyy-MM-dd') //=> '2014-02-11' |
Станет ли Темпорал нашим новым спасителем?
MomentJs, Luxon и date-fns великолепны, но разве мы не заслуживаем чего-то более нативного? Да, мы определенно это делаем, и таким образом родился Temporal API.
Temporal JS стремится заменить старый объект Date JS, предоставляя современные функции даты и времени. Он уже находится на этапе 3, что означает, что он довольно стабилен и в конечном итоге появится в наших браузерах.
Я рассматриваю Temporal API как огромное улучшение, приносящее много функциональности. Тем не менее, я не хочу перечислять, что вы можете прочитать в его документации. Вместо этого давайте сосредоточимся на некоторых существенных моментах.
Один тип для каждой проблемы
Основное улучшение заключается в том, что Temporal поддерживает концепцию один специализированный тип для одного варианта использования. Чтобы лучше понять его значение, мы должны посмотреть, как мы работаем со временем в нашей повседневной жизни.
Люди используют разные представления времени для разных случаев.
Вы не говорите:
У меня день рождения 5 мая 2023 года, затем 5 мая 2024 года, а затем 5 мая 2025 года.
Вместо этого вы говорите:
Я устрою вечеринку в пятницу 5.5, принеси свой торт.
Соответственно, очень редко вы заявляете:
Я буду там сегодня в 22:30 по всемирному координированному времени плюс 5 часов.
Вы говорите:
дайте мне час, чтобы добраться туда.
Temporal API учитывает эти сценарии из реальной жизни и предлагает набор типов для многочисленных модельных ситуаций. В то время как класс JS Date предлагает один единственный объект для каждой ситуации, Temporal имеет несколько конкретных типов для наших случаев использования. Наиболее полезными из них являются следующие:
PlainDateTime представляет дату и время по настенным часам без информации о часовом поясе. Если вы не хотите иметь дело с часовыми поясами, вам подойдет PlainDateTime
.
ZonedDateTime представляет точную дату и время с информацией о часовом поясе. Это прямая замена старому объекту JS Date.
PlainDate представляет календарную дату без часового пояса или времени.
PlainTime представляет только время. Независимо от часового пояса или даты.
Мгновенный представляет момент времени с точностью до наносекунд — без часового пояса или календаря. Он представляет собой количество целых наносекунд с эпохи Unix.
Продолжительностьвыражает продолжительность времени. Вы будете использовать его для арифметики даты и времени.
Я не включил менее важные временные типы, поэтому, если вы хотите узнать больше, не стесняйтесь просматривать этот превосходный краткий обзор API.
Явность
Следующий пункт относится к предыдущему, в котором мы узнали, что Temporal предлагает вам более одного типа. Это означает, что вы должны быть точными (то есть явными) при выборе правильного типа для вашего варианта использования. Поэтому, когда вам нужно работать со временем, вы должны использовать PlainTime
. Принимая во внимание, что если для вашего варианта использования требуется часовой пояс, используйте класс ZonedDateTime
.
Кроме того, автор Temporal принял несколько разумных решений, чтобы расширить его четкость. Например, тип Instant
(представляющий текущее время) не имеет свойства day
, month
или year
. На первый взгляд, это не имеет смысла. Как вы, наверное, знаете, вы можете написать следующий код со старой датой JS:
const month = new Date().getMonth()
С другой стороны, при использовании Temporal.Instant
вы должны сначала указать часовой пояс, чтобы получить эти даты:
const instant = Temporal.Instant.from('2022-01-01T00:00+00:00') | |
console.log(instant.hour) // returns undefined! | |
const zdtTokyo = instant.toZonedDateTimeISO('Asia/Tokyo') | |
console.log(zdtTokyo.hour) //returns 9 | |
const zdtPrague = instant.toZonedDateTimeISO('Europe/Prague') | |
console.log(zdtPrague.hour) //returns 1 | |
const zdtNewYork = instant.toZonedDateTimeISO('America/New_York') | |
console.log(zdtNewYork.hour) //returns 19 |
Немного подумав, вы поймете, что такой подход имеет смысл. Хотя у вас может быть точный момент времени, вы не можете сказать, какой сейчас час или день, без указания часового пояса. Этот тип явности означает отсутствие сюрпризов, таких как скрытые часовые пояса в сериализации JSON:
//equals `2021-01-31T23:00:00.000Z` in my time zone | |
new Date(2021,1,1).toISOString() |
Неизменяемость по умолчанию
Неизменяемость доказала свою ценность много лет назад, и Temporal поддерживает эту концепцию, как вы можете видеть здесь:
const today = Temporal.Now.plainDateISO() | |
const tomorrow = today.add({ days: 4, months: 2 }).toString() | |
//The value of today remains today. |
Temporal предлагает метод with
, полезный для изменения экземпляров Temporal:
const date = Temporal.PlainDate.from('2023-05-10') | |
const newDate1 = date.with({ day: 9 }) | |
console.log(newDate1.toString()) // => 2023-05-09 | |
const dateTime = Temporal.PlainDateTime.from('2023-05-10T11:58:32') | |
const newDateTime = dateTime.with({ year: 2024, minute: 10, hour: 5 }) | |
console.log(newDateTime.toString()) // => 2024-05-10T05:10:32 |
Переменная date
не изменилась, а newDate
имеет те же свойства, что и date
, за исключением day
. Это напоминает язык F#, который имеет ту же функцию. Однако эта функция работает со всеми экземплярами, а не только с DateTime
экземплярами.
Огромный набор функционала
Temporal поддерживает весь набор функций для работы с датой и временем. Например, методы since
, until
и equals
охватывают простое сравнение, а вычитание и сложение охватывают манипуляции с датами. Более того, Temporal API также поддерживает преобразование PlainDate
в ZonedDateTime
и наоборот. Наконец, также поддерживается преобразование между временными типами и устаревшими датами JS.
const legacyDate = new Date('1970-01-01T00:00:01Z') | |
const instant = legacyDate.toTemporalInstant() | |
const zoned= legacyDate.toZonedDateTime({timeZone, calendar}) | |
const zonedISO = legacyDate.toZonedDateTimeISO() | |
// and more ... |
Чтобы получить обзор функциональности Temporal, обратитесь к TC39 Cookbook, чтобы узнать больше канонических примеров.
Сравнение
Я немного разочарован отсутствием операторов сравнения для Temporals. В результате вы не можете так же легко сравнивать экземпляры Temporal, как со старой JS Date. Вам нужно использовать статический метод для сравнения. Однако есть загвоздка! Легко ошибиться, используя разные типы для сравнения. В следующем примере я выполняю сравнения, используя два типа: PlainDate
и PlainDateTime
. Использование разных типов дает разные результаты:
const dt1 = Temporal.PlainDateTime.from({ | |
year: 2022, | |
month: 12, | |
day: 7, | |
hour: 15 | |
}); | |
const dt2 = dt1.with({ | |
hour: 16 | |
}) | |
Temporal.PlainDate.compare(dt1, dt2) == 0 // => true | |
Temporal.PlainDateTime.compare(dt1, dt2) == 0 // => false |
Методам, к сожалению, не хватает ясности. Вы можете сравнить PlainDateTime
с PlainDate
без каких-либо ошибок, которые могут привести к ошибкам. Операторы <
, >
решили бы эту проблему.
Поддержка арифметики часовых поясов
Вы видели вышеупомянутое видео о часовых поясах? Тогда вы понимаете, почему зональная арифметика — сложная тема. Temporal API предоставляет довольно надежное решение для устранения всех странностей, таких как переход на летнее время, нечетные часовые пояса и неоднозначность времени. Под неоднозначностью времени я подразумеваю разницу между часовым временем и точным временем. Например, в США 1:30 утра 13 марта 2022 года произошло дважды из-за перехода на летнее время. Это может быть хорошо для людей, но плохо для разработчиков программного обеспечения. Подробнее можно прочитать в Временной кулинарной книге.
Форматирование
У меня для вас плохие новости: форматирование по-прежнему кажется ограниченным. Было немного горячих дискуссий по этому поводу, но пока что Temporal API поддерживает только Intl.DateTimeFormat
. См. следующий пример:
// Old way | |
format(Date.now(), 'yyyy-MM-dd HH:mm:ss') | |
// returns 2022-10-25 07:43:16 | |
// New way | |
Temporal.Now.zonedDateTimeISO() | |
.toPlainDate() | |
.toLocaleString('en-US', { year: '2-digit', month: 'long', day: 'numeric' }) | |
// returns October 22, 25 |
Похоже, что потенциальный язык мини-форматирования, такой как yyyy-MM-dd HH:mm:ss
, в будущем должен быть предоставлен библиотекой сторонних разработчиков 3D.
Поддержка календарей
Temporal API поддерживает календари, отличные от только ISO. Это может показаться немного ненужнымс точки зрения нашего западного мира. Однако в наши дни и в эпоху развивающихся рынков, таких как Китай и Индия, это стало неотъемлемой частью каждой зрелой библиотеки. Подробнее читайте в статье о написании кросс-календарного кода.
Балансировка
Теперь поговорим о продолжительности времени. Как вы, вероятно, можете себе представить, мы можем представить продолжительность времени во многих формах. Например, два часа равняются 180 минутам или 7 200 секундам — в разных случаях использования нужны разные форматы. Хорошей новостью является то, что Temporal API поддерживает преобразование в разные форматы посредством балансировки:
duration = Temporal.Duration.from({ minutes: 80, seconds: 90 }) | |
// => 80 minutes, 90 seconds | |
duration.round({ largestUnit: 'hour' }) | |
// => 1 hour, 21 minutes, 30 seconds | |
duration.round({ largestUnit: 'minute' }) | |
// => 81 minutes, 30 seconds | |
duration.round({ largestUnit: 'second' }) | |
// => 4890 seconds |
Вы можете использовать удобный метод round
для преобразования длительности в различные представления. Взгляните на официальную документацию, чтобы узнать больше.
Поддерживать
Теперь самая грустная часть моего текста. Поддержка Temporal API по-прежнему ужасна. Еще ни один браузер не реализовал его, и ни один браузер даже не сообщил о том, что в ближайшее время появятся какие-либо вехи.
Однако с другой стороны есть полифилл, который набирает обороты, как показывает график загрузок:
Таким образом, я чувствую, что вы можете начать думать о внедрении Temporal. Я протестировал полифилл, и его качество выглядит хорошо. Тем не менее, по мнению авторов, некоторая оптимизация все же должна быть проведена.
Заключение
Несмотря на то, что я могу себе представить некоторые улучшения, такие как использование операторов типа Luxon (‹ › ==), я с нетерпением жду, когда Temporal API будет встроен в наши браузеры. Это лучший API, чем JS Date на многих уровнях. Кроме того, документация хорошо написана и содержит такие вспомогательные материалы, как удобная кулинарная книга или краткий справочник.
А пока рекомендую попробовать полифилл в некритичной части вашего приложения и дождаться нативной реализации.
Мы — ACTUM Digital, и эту статью написал Марек, старший разработчик интерфейса Apollo Division.
Если вам нужна помощь с вашим проектом или инициативой, просто напишите нам. 🚀