Чому Date.parse дає неправильні результати?


345

Випадок перший:

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

Вихід:

Пт 8 липня 2005 00:00:00 GMT-0700 (PST)

Справа друга:

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

Вихід:

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


Чому другий синтаксичний аналіз є неправильним?


30
Другий синтаксичний аналіз сам по собі не є помилковим, це лише те, що перший розбирається за місцевим часом, а другий - за UTC. Зауважте, що "Четвер 07 липня 2005 17:00:00 GMT-0700 (PST)" - це те саме, що "2005-07-08 00:00".
jches


18

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. ddd MMM DD РРРР HH: мм: ss ZZ [(назва часового поясу)],
    наприклад, вт 10 липня 2018 18:39:58 GMT + 0530 (IST)
  2. ddd, DD MMM РРРР HH: мм: ss Z
    напр. Вт 10 липня 2018 13:09:58 GMT

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

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

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

Відмінно, мені довелося використовувати це, оскільки 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 або нову дату (рядок) ніколи.
Гофманн

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, коли ця відповідь спочатку була написана за 1.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 (YYYY-MM-DD), потрібно розбирати як 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, який ви можете безпечно проаналізувати у веб-переглядачах, - це довга форма зі зміщенням (або HH: мм, або "Z"). Якщо ви це зробите, ви можете сміливо повертатися вперед і назад між місцевим та UTC часом.

Це працює у веб-переглядачах (після IE9):

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

Більшість сучасних веб-переглядачів однаково ставляться до інших форматів введення, включаючи часто використовувані "1/1/1970" (M / D / РРРР) та "1/1/1970 00:00:00 AM" (M / D / РРРР-год. ​​Год. : мм: сс ап) формати. Усі наведені нижче формати (крім останнього) розглядаються як локальний вхід часу у всіх браузерах. Вихід цього коду однаковий у всіх браузерах мого часового поясу. Останній трактується як -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і 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"

Я сподіваюся, що ця відповідь була корисною.


3
По-перше, це як фантастичне написання. Однак я хотів вказати на залежність. Що стосується специфікаторів часового поясу, ви заявили: "Відсутність специфікатора повинна припускати місцеве введення часу". На щастя, стандарт ECMA-262 усуває будь-яку необхідність припускати. У ньому зазначається : "Значення відсутнього зміщення часового поясу" Z "." Отже, рядок дати / часу без вказаного часового поясу вважається UTC, а не місцевим часом. Звичайно, як і з багатьма речами JavaScript, здається, що між реалізаціями є незначна згода.
Даніель

2
... включаючи найбільш часто використовувані формати "1/1/1970" та "1/1/1970 00:00:00 AM". - найчастіше використовується де ? Це точно не в моїй країні.
ulidtko

2
@ulidtko - Вибачте, я в США. Нічого ... ти тут у Києві. Я сподіваюся, що ви та ваша родина залишатиметесь у безпеці, і незабаром все там стабілізується. Бережіть себе і удачі з усім.
drankin2112

Тут просто примітка. Здається, що це не працює для браузерів Safari (тобто iOS або OSX). З цим чи у мене виникає якесь інше питання.
keyneom

1
@ Daniel - на щастя, автори ECMAScript виправили свою помилку щодо відсутнього часового поясу для подання дати та часу. Тепер рядки дати та часу без часового поясу використовують зміщення часового поясу хоста (тобто "локальне"). Конфузно те, що форми ISO 8601 з датою трактуються як UTC (хоча це не особливо зрозуміло з специфікації), тоді як ISO 8601 трактує їх як локальні, тому вони не все виправили.
RobG

70

Існує якийсь метод до божевілля. Як правило, якщо браузер може інтерпретувати дату як ISO-8601, це буде. "2005-07-08" потрапляє в цей табір, і тому його аналізують як UTC. "8 липня 2005 р." Не може, і тому вона розбирається за місцевим часом.

Дивіться JavaScript та дати, який безлад! для більш.


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 формат "yyyy / mm / dd" вирішує звичайні проблеми. Він каже: "Дотримуйтесь" РРР / ММ / ДД "для рядків дати, коли це можливо. Це універсально підтримується і однозначно. З цим форматом усі часи є локальними". Я встановив тести: http://jsfiddle.net/jlanus/ND2Qg/432/ Цей формат: + дозволяє уникнути двозначності замовлень на день та місяць, використовуючи замовлення ymd та чотиризначний рік + уникає значення UTC порівняно з місцевою проблемою не відповідність формату ISO за допомогою використання косої риски + danvk, хлопець із малюнків , говорить, що цей формат хороший у всіх браузерах.


Ви можете побачити відповідь автора .
Бред Кох

Я б сказав, що рішення з прикладом у jsFiddle працює досить добре, якщо ви використовуєте jQuery, оскільки він використовує аналізатор datepicker. У моєму випадку проблема з jqGrid, але виявлено, що у неї є метод parseDate. Але все-таки приклад допоміг мені, давши мені ідею, тому +1 за це, дякую.
Василь Попов

2
Ця стаття про малюнки помилкова, і перший приклад на сторінці чітко ілюструє, чому використання косої риски замість дефісів - це погана порада. На момент написання статті за допомогою "2012/03/13" браузер розглядав її як місцеву дату, а не UTC. Специфікація ECMAScript визначає явну підтримку використання "YYYY-MM-DD" (ISO8601), тому завжди використовуйте дефіси. Слід зазначити, що під час написання цього коментаря Chrome був виправлений, щоб трасувати косої риси як UTC.
Лаклан Хант

4

Хоча CMS правильно, що передавання рядків у метод розбору, як правило, небезпечно, нова специфікація ECMA-262 5th Edition (також ES5) у розділі 15.9.4.2 передбачає, щоDate.parse() насправді слід обробляти дати у форматі ISO. Стара специфікація такої претензії не пред'являла. Звичайно, старі браузери та деякі поточні браузери все ще не забезпечують цю функціональність ES5.

Ваш другий приклад не помиляється. Це вказана дата в UTC, як мається на увазі Date.prototype.toISOString(), але представлена ​​у вашому місцевому часовому поясі.


1
І розбір рядків дат знову змінено в ECMAScript 2015, так що "2005-07-08" є локальним, а не UTC. BTW, ES5 не був стандартом до червня 2011 року (а зараз ECMAScript 2015). ;-)
RobG

1
Тільки щоб заплутати речі, TC39 вирішив у жовтні (лише через місяць після мого попереднього повідомлення), що "2005-07-08" повинен бути UTC , однак "" 2005-07-08T00: 00: 00 "повинен бути локальним. Обидва ISO 8601 сумісні формати, обидва без часового поясу, але по-різному трактуються. Перейдіть до рисунку.
RobG

2

Ця невелика бібліотека дати розбору дат повинна вирішити всі подібні проблеми. Мені подобається бібліотека, тому що її досить легко розширити. Це також можливо i18n (не дуже прямо, але не так важко).

Приклад розбору:

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

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"

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
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.