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

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

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)


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

11 ответов

Решение

До выхода 5-го издания Date.parse метод полностью зависит от реализации (new Date(string) эквивалентно Date.parse(string) кроме последнего возвращает число, а не 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 по Гринвичу

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

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

// parse a date in yyyy-mm-dd format
function parseDate(input) {
  var 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
}

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

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

Все реализации хранят свои значения даты внутри как 64-битные числа, которые представляют количество миллисекунд с 01.01.1970 UTC (GMT - то же самое, что и UTC). Даты, происходящие после 1/1/1970 00:00:00 являются положительными числами, а даты до являются отрицательными.

Поэтому следующий код дает одинаковый результат во всех браузерах.

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

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

Вот загвоздка, хотя. Хотя в форматах входных строк есть некоторая разница, которую основные браузеры будут анализировать как даты, они, по сути, интерпретируют их так же, как часовые пояса и переход на летнее время. Одним из них является формат ISO 8601. Это единственный формат, описанный в спецификации ECMA-262 v.5. Для всех других форматов строк интерпретация зависит от реализации. По иронии судьбы, это формат, в котором браузеры могут отличаться. Вот сравнительный вывод 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 и не требует смещения перед сохранением.
  • Спецификатор "-0500" указывает, что ввод в GMT-05:00, поэтому оба браузера интерпретируют ввод как находящийся в моем местном часовом поясе. Это означает, что значение преобразуется в UTC перед сохранением. В моем случае это означает добавление 18000000 мс к внутреннему значению даты, поэтому требуется сдвиг -18000000 мс (-05:00), чтобы вернуть меня по местному времени.
  • Однако при отсутствии спецификатора FF обрабатывает ввод как местное время, а Chrome - как время UTC. Для меня это создает 5-часовую разницу в сохраненном значении, что проблематично. В моей реализации я остановился на FF, потому что мне нравится вывод toString соответствовать моему входному значению, если я не укажу альтернативный часовой пояс, чего я никогда не делаю. Отсутствие спецификатора должно предполагать ввод местного времени.

Но здесь ситуация ухудшается: FF обрабатывает краткую форму формата ISO 8601 ("ГГГГ-ММ-ДД") иначе, чем длинную форму ("ГГГГ-ММ-ДДГЧ: мм: сс: ссс Z") для нет логической причины вообще. Вот вывод из FF с длинными и короткими форматами даты ISO без указания часового пояса.

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

Итак, чтобы ответить на первоначальный вопрос Аскера напрямую, "YYYY-MM-DD" это краткая форма формата ISO 8601 "YYYY-MM-DDTHH:mm:ss:sssZ", Таким образом, оно интерпретируется как время UTC, а другое - как локальное. Вот почему,

Это не джайв

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

Это делает:

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());

К счастью, большинство современных браузеров обрабатывают другие форматы ввода одинаково, включая наиболее часто используемые форматы "01.01.1970" и "01.01.1970 00:00:00 AM". Все следующие форматы (и другие) обрабатываются как ввод местного времени во всех браузерах и преобразуются в UTC перед хранением. Таким образом, делая их кросс-браузерными. Вывод этого кода одинаков во всех браузерах в моем часовом поясе.

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"));

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

Что касается вывода, все браузеры переводят часовые пояса одинаково, но по-разному обрабатывают строковые форматы. Вот toString функции и что они выводят. Обратите внимание на toUTCString а также toISOString функции вывода 5: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"

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

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

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

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

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

Пример:

    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'];

Используйте 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 также может давать неверные результаты.

Согласно 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, говорит, что этот формат хорош во всех браузерах.

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

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

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

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

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") );

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

// 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"

Вот короткий гибкий фрагмент для преобразования строки даты и времени в кросс-браузерно-безопасный способ, как nicel подробно описано в @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()

Принятый ответ от 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
}
Другие вопросы по тегам