JavaScript считает все данные датами
- четверг, 26 марта 2026 г. в 00:00:07
Excel не единственный, кто любит превращать любые данные в даты.

Если вы работаете с датами в JavaScript, то, вероятно, рано или поздно воспользуетесь new Date(someString). Это удобно: передаём строку, получаем объект Date. Но привыкнув к Python, я был удивлён тем, насколько свободно JavaScript обращается с форматами дат. Позвольте мне проиллюстрировать это примерами.
new Date("2020-01-23") // Wed Jan 22 2020 19:00:00 GMT-0500
Вполне логично. Формат ISO, полночь UTC, поэтому он показывает 22 января в Западном полушарии.
new Date("Today is 2020-01-23") // Thu Jan 23 2020 00:00:00 GMT-0500
Что ж, он вытянул дату из предложения, что в некоторых случаях может быть полезно. Любопытно, что время сместилось, и это немного странно.
new Date("Route 66") // Sat Jan 01 1966 00:00:00 GMT-0500
Он считает, что «Route 66» подразумевает 1966 год? Ну это определённо явная натяжка.
new Date("Beverly Hills, 90210") // Mon Jan 01 90210 00:00:00 GMT-0500
90210 год? Вы что, смеётесь?!
Парсер может запросто сбоить, и в этом случае конструктор создаёт невалидный объект Date, который легко проверить. Однако в этих примерах он создаёт валидные, но некорректные Date, которые сложнее выявить в коде.
Можете мне не верить, но это не баги! Парсер работает, «как задумано».
Спецификация ECMAScript требует лишь, чтобы new Date() парсил один формат: подмножество ISO 8601 (вида "2025-11-06" или "2025-11-06T10:30:00Z"). Во всех остальных случаях в спецификации говорится, что поведение «определяется реализацией». На практике, и V8 (Chrome, Edge, Node.js), и SpiderMonkey (Firefox) имеют легаси-парсеры, упорно пытающиеся извлечь дату из всего, что им передашь. (Движок Safari JavaScriptCore в этом отличается. Мы вернёмся к этому ниже.)
В реализации V8 правила примерно такие:
Движок сначала пытается спарсить строку, как ISO 8601. Если строка не начинается с четырёхзначного числа или символа знака, то парсер ISO сразу сдаётся.
Управление передаётся легаси-парсеру, который сканирует строку токен за токеном. Все нераспознанные слова, встречающиеся до первого числа, беззвучно игнорируются.
Числа, за которыми не следует двоеточие (превращающее их в компонент времени), передаются в DayComposer, собирающий год, месяц и день.
Отдельное число в интервале 1-12 считается месяцем.
Числа в интервале 13-31 считаются неоднозначными (день без месяца?) и заставляют парсер отклонить строку.
Числа от 32 и выше считаются годами. К числам 50-99 прибавляется 1900, чтобы соответствовать стандарту лет из двух разрядов.
Если месяц и день не указаны, то по умолчанию равны 1.
Это означает, что:
new Date("Route 66") без проблем возвращает 1 января 1966 года, потому что 66 больше 31, из-за чего срабатывает правило года из двух разрядов.
new Date("Beverly Hills, 90210") тоже работает. 90210 намного больше интервала из двух разрядов, поэтому используется в неизменном виде: Year 90210, January 1st.
Однако new Date("Catch 22") возвращает Invalid Date, потому что 22 находится в «мёртвой зоне» от 13 до 31. Хвала небесам! Преобразование "Catch 22" в дату было бы абсурдом!
Есть и ещё более тонкая проблема: у двух парсеров есть разные правила относительно часовых поясов. В спецификации говорится, что строки ISO, состоящие из одной даты, например, "2020-01-23", должны парситься как UTC. Но легаси-парсер по умолчанию использует местное время. Это приводит к следующему:
new Date("2020-01-23") // Wed Jan 22 2020 19:00:00 GMT-0500 // Полночь UTC - это 22 января в Западной полусфере new Date("Today is 2020-01-23") // Thu Jan 23 2020 00:00:00 GMT-0500 // Местная полночь, поэтому день остаётся 23 января
Одна и та же календарная дата в строке, но разный вывод. Первая попадает в парсер ISO (UTC). Вторая начинается с «Today», из-за чего парсер ISO отказывается её обрабатывать, поэтому ею занимается легаси-парсер (местное время). Достаточно даже пробела в начале или конце, чтобы за дело взялся легаси-парсер.
Движок Safari JavaScriptCore гораздо строже. Он отклоняет все перечисленные выше нездоровые примеры. Он даже отклоняет строки с пробелом в конце (хотя пробел в начале почему-то допускается).
Ирония заключается в том, что комментарий в исходном коде V8 обвиняет в существовании этой легаси-бессмыслицы браузер Safari.
// Принимает ES5 ISO 8601 date-time-strings // или легаси-даты, совместимые с Safari.
Думаю, разработчики Safari решили умыть руки и двигаться дальше, но Chrome и Firefox по-прежнему живут прошлым.
У нас была функция, пробовавшая использовать множество парсеров дат на основе regex, а потом откатывающаяся к new Date() в случае всего, что не соответствует регулярным выражениям. Этот откат должен был стать мерой защиты для форматов дат, которые мы не предвидели. Однако вместо этого парсер начал безмолвно преобразовывать в даты произвольный текст. Пользователи наблюдали ячейки таблиц, в которых названия улиц и идентификаторы отображались в виде отформатированных дат.
Мы устранили проблему, избавившись от этого отката. Наши собственные парсеры уже обрабатывали все нужные нам форматы. Если строка не соответствовала ни одному известному паттерну, то это была не дата.
Здесь стоит провести сравнение с решением в Python. В стандартной библиотеке Python нет эквивалента new Date(string). В ней нет функции, получающей произвольную строку, которая пытается угадать, содержит ли она дату.
datetime.fromisoformat() принимает только ISO 8601. Всё остальное возвращает ValueError. Метод datetime.strptime() позволяет парсить другие форматы, но он всё равно очень строгий и не допускает даже лишних пробелов.
Это соответствует Дзену Пайтон: «Встретив двусмысленность, отбрось искушение угадать». Легаси-парсер JavaScript использует обратный подход: он агрессивно гадает и безмолвно возвращает правдоподобно выглядящие ошибочные ответы. Громкая ошибка почти всегда предпочтительнее, чем безмолвный неверный ответ.
Вызывайте new Date(string) только тогда, когда контролируете формат входных данных и знаете, что он будет парситься так, как вы ожидаете. В случае передаваемых пользователем данных сначала валидируйте формат при помощи regex или используйте строгий парсер наподобие parseISO() date-fns. Переходите на Temporal, если она имеет достаточно обширную поддержку для вашего сценария. Не относитесь к конструктору Date, как к валидатору.
А если вы проектируете язык программирования, который станет самым популярным в мире, то хотя бы убедитесь, что парсер данных отклоняет названия телесериалов.