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

Случай один:

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

Вывод:

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

Вариант второй:

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

Вывод:

Thu Jul 07 2005 17:00:00 GMT-0700 (PST)


Почему второй синтаксический разбор неверен?

Ответ 1

До появления спецификации 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) формат даты # toString и Date # toUTCString был указан как (соответственно):

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

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

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

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

Ответ 2

Во время недавнего опыта написания 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 vs Firefox для 1/1/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 ( "YYYY-MM-DD" ) иначе, чем обрабатывает длинную форму ( "YYYY-MM-DDTHH: mm: ss: sssZ" ) без каких-либо логических причин. Вот результат из 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, тогда как другое интерпретируется как локальное. Поэтому,

Это не jive:

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

К счастью, большинство современных браузеров одинаково обрабатывают другие форматы ввода, включая наиболее часто используемые форматы '1/1/1970' и '1/1/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 AM на моем компьютере.

Преобразование из UTC в локальное время перед печатью

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

Прямая печать сохраненного времени UTC

 - toUTCString
 - toISOString 

In 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

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

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

Ответ 3

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

Подробнее см. JavaScript и даты, какой беспорядок!.

Ответ 4

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

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

Пример:

    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

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

Ответ 6

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

Ответ 7

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

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

Ответ 8

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

Пример анализа:

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

Ответ 9

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

Ответ 10

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

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

Ответ 11

Принятый ответ от 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
}