Почему Date.parse дает неверные результаты?

346

Первый случай:

new Date(Date.parse("Jul 8, 2005"));

Вывод:

Пт Июл 08 2005 00:00:00 GMT-0700 (PST)

Случай второй:

new Date(Date.parse("2005-07-08"));

Вывод:

Чт 07 июля 2005 17:00:00 GMT-0700 (PST)


Почему второй анализ неверен?

user121196
источник
30
Второй анализ сам по себе не является неправильным, просто первый анализируется по местному времени, а второй по UTC. Обратите внимание, что «Чт 07 июля 2005 17:00:00 GMT-0700 (PST)» совпадает с «2005-07-08 00:00».
jches
18
ISO 8601 xkcd.
ulidtko
1
Если кто-то пришел сюда, чтобы выяснить, почему дата возвращается NaNв Firefox, я обнаружил, что большинство других браузеров (и Node.js) будут анализировать дату без дня, например «Апрель 2014» на 1 апреля 2014 года, но Firefox возвращает NaN. Вы должны пройти правильную дату.
Джаззи
Чтобы добавить комментарий Джейсона выше: если вы получаете NaN в Firefox, еще одна проблема может заключаться в том, что Firefox и Safari не любят переносить даты. Только Chrome делает. Вместо этого используйте слеш.
Бангкок

Ответы:

451

До выхода спецификации 5-го издания Date.parseметод полностью зависел от реализации ( new Date(string)эквивалентно тому, Date.parse(string)что последний возвращает число, а не a Date). В спецификации 5-го издания было добавлено требование для поддержки упрощенного (и слегка некорректного) ISO-8601 (также см. Что такое допустимые строки даты и времени в JavaScript? ). Но кроме этого, не было никакого требования для того, что Date.parse/ new Date(string)должно принять, кроме того, что они должны были принять любой Date#toStringвывод (не говоря, что это было).

Начиная с ECMAScript 2017 (редакция 8), реализации должны были анализировать свои выходные данные для Date#toStringи Date#toUTCString, но формат этих строк не был указан.

Начиная с ECMAScript 2019 (редакция 9) формат для Date#toStringи Date#toUTCString, был определен как (соответственно):

  1. ддд
    ммм дд гггг чч : мм: сс ZZ [(название часового пояса)] например вт 10 июля 2018 18:39:58 GMT + 0530 (IST)
  2. ддд, дд ммм гггг чч: мм: сс Z
    например. Вт 10 июл 2018 13:09:58 GMT

предоставление еще 2 форматов, которые Date.parseдолжны надежно анализироваться в новых реализациях (с учетом того, что поддержка не является повсеместной, и несовместимые реализации будут использоваться в течение некоторого времени).

Я бы порекомендовал, чтобы строки даты анализировались вручную, а конструктор Date использовался с аргументами year, month и day, чтобы избежать двусмысленности:

// parse a date in yyyy-mm-dd format
function parseDate(input) {

  let parts = input.split('-');

  // new Date(year, month [, day [, hours[, minutes[, seconds[, ms]]]]])
  return new Date(parts[0], parts[1]-1, parts[2]); // Note: months are 0-based
}
CMS
источник
Отлично, я должен был использовать это, потому что Date.parseне вел себя с британскими форматами даты по какой-то причине, которую я не мог отработать
Бен
1
Части времени документированы в коде @CMS. Я использовал этот код с форматом даты «2012-01-31 12:00:00» return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]);работает отлично, спасибо!
Ричард Раут
1
@CMS что вы подразумеваете под зависимостью от реализации ?
Рой Намир
3
@RoyiNamir, это означает, что результаты зависят от того, какой веб-браузер (или другая реализация JavaScript) выполняет ваш код.
Сэмюэль Эдвин Уорд
1
У меня также были проблемы с новой датой (строкой) в разных браузерах, которые ведут себя по-разному. Дело даже не в том, что это сломано на старых версиях IE, разные браузеры просто не совместимы. Никогда не используйте Date.parse или новую Date (строку).
Хоффманн
195

Во время недавнего опыта написания интерпретатора JS я много боролся с внутренней работой дат ECMA / JS. Итак, я полагаю, что я добавлю свои 2 цента здесь. Надеюсь, что обмен этими материалами поможет другим с любыми вопросами о различиях между браузерами в том, как они обрабатывают даты.

Сторона ввода

Все реализации хранят свои значения даты внутри как 64-битные числа, которые представляют количество миллисекунд (мс) с 1970-01-01 UTC (GMT - то же самое, что и UTC). Эта дата является эпохой ECMAScript, которая также используется другими языками, такими как системы Java и POSIX, такими как UNIX. Даты, происходящие после эпохи, являются положительными числами, а даты до являются отрицательными.

Следующий код интерпретируется как одна и та же дата во всех текущих браузерах, но со смещением местного часового пояса:

Date.parse('1/1/1970'); // 1 January, 1970

В моем часовом поясе (EST, то есть -05: 00) результат равен 18000000, потому что это количество мс за 5 часов (это всего 4 часа в летнее время). Значение будет отличаться в разных часовых поясах. Это поведение указано в ECMA-262, поэтому все браузеры делают это одинаково.

Хотя в форматах входных строк есть некоторые различия, которые основные обозреватели будут анализировать как даты, они, по сути, интерпретируют их так же, как часовые пояса и переход на летнее время, даже если синтаксический анализ в значительной степени зависит от реализации.

Однако формат ISO 8601 отличается. Это один из двух форматов, специально описанных в ECMAScript 2015 (ред. 6), который должен быть проанализирован одинаково во всех реализациях (другой - формат, указанный для Date.prototype.toString ).

Но даже для строк формата ISO 8601 некоторые реализации ошибаются. Вот сравнительный вывод Chrome и Firefox, когда этот ответ был изначально написан для моей машины 01.01.1970 (эпоха), используя строки формата ISO 8601, которые должны быть проанализированы с одинаковым значением во всех реализациях:

Date.parse('1970-01-01T00:00:00Z');       // Chrome: 0         FF: 0
Date.parse('1970-01-01T00:00:00-0500');   // Chrome: 18000000  FF: 18000000
Date.parse('1970-01-01T00:00:00');        // Chrome: 0         FF: 18000000
  • В первом случае спецификатор «Z» указывает на то, что вход находится во времени UTC, поэтому он не смещен от эпохи, и результат равен 0
  • Во втором случае спецификатор «-0500» указывает, что ввод происходит в GMT-05: 00, и оба браузера интерпретируют ввод как находящийся в часовом поясе -05: 00. Это означает, что значение UTC смещено относительно эпохи, что означает добавление 18000000 мс к значению внутреннего времени даты.
  • Третий случай, когда нет спецификатора, должен рассматриваться как локальный для хост-системы. FF правильно обрабатывает ввод как местное время, в то время как Chrome обрабатывает его как UTC, таким образом производя различные значения времени. Для меня это создает 5-часовую разницу в сохраненном значении, что проблематично. Другие системы с другим смещением получат разные результаты.

Это различие было исправлено с 2020 года, но существуют другие особенности браузеров при разборе строк в формате ISO 8601.

Но это становится хуже. Причудой ECMA-262 является то, что формат ISO 8601 только для даты (ГГГГ-ММ-ДД) должен быть проанализирован как UTC, тогда как ISO 8601 требует, чтобы он был проанализирован как локальный. Вот вывод из FF с длинными и короткими форматами даты ISO без указания часового пояса.

Date.parse('1970-01-01T00:00:00');       // 18000000
Date.parse('1970-01-01');                // 0

Поэтому первый анализируется как локальный, потому что это дата и время ISO 8601 без часового пояса, а второй - как UTC, потому что это только дата ISO 8601.

Таким образом, "YYYY-MM-DD"ECMA-262 требует прямого ответа на исходный вопрос как UTC, а другой - как локальный. Поэтому:

Это не дает эквивалентных результатов:

console.log(new Date(Date.parse("Jul 8, 2005")).toString()); // Local
console.log(new Date(Date.parse("2005-07-08")).toString());  // UTC

Это делает:

console.log(new Date(Date.parse("Jul 8, 2005")).toString());
console.log(new Date(Date.parse("2005-07-08T00:00:00")).toString());

Суть в том, что это для разбора строк даты. ЕДИНСТВЕННАЯ строка ISO 8601, которую можно безопасно анализировать в разных браузерах, - это длинная форма со смещением (либо ± ЧЧ: мм, либо «Z»). Если вы сделаете это, вы можете безопасно переходить между местным и UTC временем.

Это работает во всех браузерах (после IE9):

console.log(new Date(Date.parse("2005-07-08T00:00:00Z")).toString());

Большинство современных браузеров обрабатывают другие форматы ввода одинаково, включая часто используемые «1/1/1970» (M / D / YYYY) и «01.01.1970 00:00:00 AM» (M / D / YYYY чч : mm: ss ap) форматы. Все следующие форматы (кроме последнего) обрабатываются как ввод местного времени во всех браузерах. Вывод этого кода одинаков во всех браузерах в моем часовом поясе. Последний обрабатывается как -05: 00 независимо от часового пояса хоста, поскольку смещение установлено в метке времени:

console.log(Date.parse("1/1/1970"));
console.log(Date.parse("1/1/1970 12:00:00 AM"));
console.log(Date.parse("Thu Jan 01 1970"));
console.log(Date.parse("Thu Jan 01 1970 00:00:00"));
console.log(Date.parse("Thu Jan 01 1970 00:00:00 GMT-0500"));

Однако, поскольку синтаксический анализ даже форматов, указанных в ECMA-262, не является согласованным, рекомендуется никогда не полагаться на встроенный синтаксический анализатор и всегда анализировать строки вручную, например, используя библиотеку, и предоставлять формат анализатору.

Например, в moment.js вы можете написать:

let m = moment('1/1/1970', 'M/D/YYYY'); 

Выходная сторона

Что касается вывода, все браузеры переводят часовые пояса одинаково, но по-разному обрабатывают строковые форматы. Вот toStringфункции и что они выводят. Обратите внимание на вывод функций toUTCStringи toISOString5:00 на моей машине. Кроме того, имя часового пояса может быть аббревиатурой и может отличаться в разных реализациях.

Преобразует из UTC в Местное время перед печатью

 - toString
 - toDateString
 - toTimeString
 - toLocaleString
 - toLocaleDateString
 - toLocaleTimeString

Печатает сохраненное время UTC напрямую

 - toUTCString
 - toISOString 

В Chrome
toString            Thu Jan 01 1970 00:00:00 GMT-05:00 (Eastern Standard Time)
toDateString        Thu Jan 01 1970
toTimeString        00:00:00 GMT-05:00 (Eastern Standard Time)
toLocaleString      1/1/1970 12:00:00 AM
toLocaleDateString  1/1/1970
toLocaleTimeString  00:00:00 AM

toUTCString         Thu, 01 Jan 1970 05:00:00 GMT
toISOString         1970-01-01T05:00:00.000Z

В Firefox
toString            Thu Jan 01 1970 00:00:00 GMT-05:00 (Eastern Standard Time)
toDateString        Thu Jan 01 1970
toTimeString        00:00:00 GMT-0500 (Eastern Standard Time)
toLocaleString      Thursday, January 01, 1970 12:00:00 AM
toLocaleDateString  Thursday, January 01, 1970
toLocaleTimeString  12:00:00 AM

toUTCString         Thu, 01 Jan 1970 05:00:00 GMT
toISOString         1970-01-01T05:00:00.000Z

Я обычно не использую формат ISO для ввода строки. Единственный раз, когда использование этого формата выгодно для меня, это когда даты должны быть отсортированы в виде строк. Формат ISO сортируется как есть, а остальные нет. Если вам нужна совместимость с разными браузерами, укажите часовой пояс или используйте совместимый формат строки.

Код new Date('12/4/2013').toString()проходит через следующее внутреннее псевдопреобразование:

  "12/4/2013" -> toUCT -> [storage] -> toLocal -> print "12/4/2013"

Я надеюсь, что этот ответ был полезным.

drankin2112
источник
3
Во-первых, это фантастическая рецензия. Однако я хотел указать на зависимость. Что касается спецификаторов часовых поясов, вы заявили: «Отсутствие спецификатора должно предполагать ввод местного времени». К счастью, стандарт ECMA-262 устраняет необходимость в допущении. В нем говорится : «Значение смещения отсутствующего часового пояса равно« Z »». Таким образом, строка даты / времени без указания часового пояса считается UTC, а не местным временем. Конечно, как и во многих других JavaScript, между реализациями, похоже, мало согласия.
Даниил
2
... включая наиболее часто используемые форматы «01.01.1970» и «01.01.1970 00:00:00 AM». - наиболее часто используется , когда ? Это не в моей стране, конечно.
ulidtko
2
@ulidtko - Извините, я в США. Ух ты ... ты прямо в Киеве. Я надеюсь, что вы и ваша семья останетесь в безопасности и скоро все стабилизируется. Береги себя и удачи во всем.
drankin2112
Просто заметка здесь. Кажется, что это не работает для браузеров Safari (то есть iOS или OSX). Это или у меня какая-то другая проблема.
Keyneom
1
@ Даниэль - к счастью, авторы ECMAScript исправили ошибку, связанную с отсутствием часового пояса для представления даты и времени. Теперь строки даты и времени без часового пояса используют смещение часового пояса хоста (то есть «локальный»). Заблуждение, что формы ISO 8601 только с датами обрабатываются как UTC (хотя это не особенно ясно из спецификации), тогда как ISO 8601 рассматривает их как локальные, поэтому они не все исправили.
RobG
70

Есть какой-то метод к безумию. Как правило, если браузер может интерпретировать дату как ISO-8601, это так. «2005-07-08» попадает в этот лагерь, и поэтому он анализируется как UTC. «8 июля 2005» не может, и поэтому анализируется по местному времени.

Смотрите JavaScript и даты, что за беспорядок! для большего.

danvk
источник
3
« Как правило, если браузер может интерпретировать дату как ISO-8601, это будет». Это не поддерживается. «2020-03-20 13:30:30» обрабатывается как ISO 8601 и локально во многих браузерах, но «Неверная дата» в Safari. Существует множество форматов ISO 8601, которые не поддерживаются большинством браузеров, например 2004-W53-7 и 2020-092.
RobG
7

Другое решение - создать ассоциативный массив с форматом даты, а затем переформатировать данные.

Этот метод полезен для даты, отформатированной необычным способом.

Пример:

    mydate='01.02.12 10:20:43':
    myformat='dd/mm/yy HH:MM:ss';


    dtsplit=mydate.split(/[\/ .:]/);
    dfsplit=myformat.split(/[\/ .:]/);

    // creates assoc array for date
    df = new Array();
    for(dc=0;dc<6;dc++) {
            df[dfsplit[dc]]=dtsplit[dc];
            }

    // uses assc array for standard mysql format
    dstring[r] = '20'+df['yy']+'-'+df['mm']+'-'+df['dd'];
    dstring[r] += ' '+df['HH']+':'+df['MM']+':'+df['ss'];
Гвидо Рицци
источник
5

Используйте moment.js для разбора дат:

var caseOne = moment("Jul 8, 2005", "MMM D, YYYY", true).toDate();
var caseTwo = moment("2005-07-08", "YYYY-MM-DD", true).toDate();

Третий аргумент определяет строгий анализ (доступно с 2.3.0). Без этого moment.js также может давать неверные результаты.

Лукаш Виктор
источник
4

Согласно http://blog.dygraphs.com/2012/03/javascript-and-dates-what-mess.html формат "гггг / мм / дд" решает обычные проблемы. Он говорит: «Придерживайтесь« ГГГГ / ММ / ДД »для ваших строк дат, когда это возможно. Это универсально поддерживается и однозначно. С этим форматом все времена локальные». Я установил тесты: http://jsfiddle.net/jlanus/ND2Qg/432/ Этот формат: + позволяет избежать двусмысленности порядка дня и месяца, используя порядок ymd и год из 4 цифр + избегает UTC по сравнению с локальной проблемой, а не соблюдение формата ISO с помощью slashes + danvk, парень dygraphs , говорит, что этот формат хорош во всех браузерах.

Хуан Ланус
источник
Возможно, вы захотите увидеть ответ автора .
Брэд Кох
Я бы сказал, что решение с примером в jsFiddle работает достаточно хорошо, если вы используете jQuery, поскольку он использует анализатор datepicker. В моем случае проблема связана с jqGrid, но обнаружил, что у него есть метод parseDate. Но в любом случае пример помог мне, дав мне идею, поэтому +1 за это, спасибо.
Василь Попов,
2
Эта статья о графах неверна, и первый пример на странице ясно показывает, почему использование косых черт вместо дефисов - действительно плохой совет. На момент написания статьи использование «2012/03/13» привело к тому, что браузер проанализировал ее как локальную дату вместо UTC. Спецификация ECMAScript определяет явную поддержку использования «ГГГГ-ММ-ДД» (ISO8601), поэтому всегда используйте дефисы. Следует отметить, что в то время, когда я пишу этот комментарий, Chrome был исправлен, чтобы рассматривать косые черты как UTC.
Лахлан Хант
4

Хотя CMS верна , передача строк в метод синтаксического анализа, как правило, небезопасна, но новая спецификация ECMA-262 5-го издания (также известная как ES5) в разделе 15.9.4.2 предполагает, что Date.parse()фактически должна обрабатывать даты в формате ISO. Старая спецификация не предъявляла таких требований. Конечно, старые браузеры и некоторые современные браузеры все еще не предоставляют эту функциональность ES5.

Ваш второй пример не ошибается. Это указанная дата в формате UTC, как подразумевается Date.prototype.toISOString(), но она указана в вашем местном часовом поясе.

Peller
источник
1
И анализ строк даты снова изменился в ECMAScript 2015, так что «2005-07-08» является локальным, а не UTC. Кстати, ES5 не был стандартом до июня 2011 года (и в настоящее время ECMAScript 2015 является). ;-)
RobG
1
Просто чтобы запутать вещи, TC39 решил в октябре (всего через месяц после моего предыдущего поста), что «2005-07-08» должен быть UTC , однако «« 2005-07-08T00: 00: 00 »должен быть локальным. Оба стандарта ISO 8601 совместимые форматы, оба без часового пояса, но обрабатываются по-разному. Пойди к
черту
2

Эта легковесная библиотека разбора даты должна решить все подобные проблемы. Мне нравится библиотека, потому что ее довольно легко расширить. Это также возможно (не очень прямо, но не так сложно).

Пример разбора:

var caseOne = Date.parseDate("Jul 8, 2005", "M d, Y");
var caseTwo = Date.parseDate("2005-07-08", "Y-m-d");

И форматирование обратно в строку (вы заметите, что оба случая дают одинаковый результат):

console.log( caseOne.dateFormat("M d, Y") );
console.log( caseTwo.dateFormat("M d, Y") );
console.log( caseOne.dateFormat("Y-m-d") );
console.log( caseTwo.dateFormat("Y-m-d") );
Nux
источник
2

Вот короткий гибкий фрагмент, позволяющий преобразовать строку даты-времени в безопасном для браузера стиле, как подробно описано в @ drankin2112.

var inputTimestamp = "2014-04-29 13:00:15"; //example

var partsTimestamp = inputTimestamp.split(/[ \/:-]/g);
if(partsTimestamp.length < 6) {
    partsTimestamp = partsTimestamp.concat(['00', '00', '00'].slice(0, 6 - partsTimestamp.length));
}
//if your string-format is something like '7/02/2014'...
//use: var tstring = partsTimestamp.slice(0, 3).reverse().join('-');
var tstring = partsTimestamp.slice(0, 3).join('-');
tstring += 'T' + partsTimestamp.slice(3).join(':') + 'Z'; //configure as needed
var timestamp = Date.parse(tstring);

Ваш браузер должен предоставить тот же результат отметки времени, что Date.parseи для:

(new Date(tstring)).getTime()
Лоренц Ло Зауэр
источник
Я бы предложил добавить T к регулярному выражению, чтобы перехватывать уже отформатированные в JS даты: inputTimestamp.split (/ [T \ /: -] / g)
andig
Если вы разделите строку на составные части, то наиболее надежным следующим шагом будет использование этих частей в качестве аргументов конструктора Date. Создание еще одной строки для передачи анализатору просто возвращает вас к шагу 1. «2014-04-29 13:00:15» должен быть проанализирован как локальный, ваш код переформатирует его как UTC. :-(
RobG
2

Оба являются правильными, но они интерпретируются как даты с двумя разными часовыми поясами. Итак, вы сравнили яблоки и апельсины:

// local dates
new Date("Jul 8, 2005").toISOString()            // "2005-07-08T07:00:00.000Z"
new Date("2005-07-08T00:00-07:00").toISOString() // "2005-07-08T07:00:00.000Z"
// UTC dates
new Date("Jul 8, 2005 UTC").toISOString()        // "2005-07-08T00:00:00.000Z"
new Date("2005-07-08").toISOString()             // "2005-07-08T00:00:00.000Z"

Я удалил Date.parse()вызов, так как он автоматически используется в строковом аргументе. Я также сравнил даты, используя формат ISO8601, чтобы вы могли визуально сравнить даты между вашими локальными датами и датами UTC. Время составляет 7 часов, что является разницей в часовых поясах и почему ваши тесты показали две разные даты.

Другой способ создания этих же локальных / UTC-дат:

new Date(2005, 7-1, 8)           // "2005-07-08T07:00:00.000Z"
new Date(Date.UTC(2005, 7-1, 8)) // "2005-07-08T00:00:00.000Z"

Но я все еще настоятельно рекомендую Moment.js, который так же прост, но мощный :

// parse string
moment("2005-07-08").format()       // "2005-07-08T00:00:00+02:00"
moment.utc("2005-07-08").format()   // "2005-07-08T00:00:00Z"
// year, month, day, etc.
moment([2005, 7-1, 8]).format()     // "2005-07-08T00:00:00+02:00"
moment.utc([2005, 7-1, 8]).format() // "2005-07-08T00:00:00Z"
DJDaveMark
источник
0

Общепринятый ответ от CMS является правильным, я просто добавил некоторые особенности:

  • обрезать и чистить места ввода
  • разбирать косые черты, тире, двоеточия и пробелы
  • имеет день и время по умолчанию

// parse a date time that can contains spaces, dashes, slashes, colons
function parseDate(input) {
    // trimes and remove multiple spaces and split by expected characters
    var parts = input.trim().replace(/ +(?= )/g,'').split(/[\s-\/:]/)
    // new Date(year, month [, day [, hours[, minutes[, seconds[, ms]]]]])
    return new Date(parts[0], parts[1]-1, parts[2] || 1, parts[3] || 0, parts[4] || 0, parts[5] || 0); // Note: months are 0-based
}
Стефан Л
источник