为啥 Date.parse 给出不正确的结果?

Posted

技术标签:

【中文标题】为啥 Date.parse 给出不正确的结果?【英文标题】:Why does Date.parse give incorrect results?为什么 Date.parse 给出不正确的结果? 【发布时间】:2010-04-06 18:35:53 【问题描述】:

案例一:

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

输出:

2005 年 7 月 8 日星期五 00:00:00 GMT-0700 (PST)

案例二:

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

输出:

2005 年 7 月 7 日星期四 17:00:00 GMT-0700 (PST)


为什么第二次解析不正确?

【问题讨论】:

第二个解析本身并没有错,只是第一个是在本地时间解析的,第二个是在 UTC 解析的。请注意,“Thu Jul 07 2005 17:00:00 GMT-0700 (PST)”与“2005-07-08 00:00”相同。 jsPerf: jsperf.com/value-of-date-input-to-date-object ISO 8601xkcd. 如果有人来这里弄清楚为什么在 Firefox 中日期返回 NaN,我发现大多数其他浏览器(和 Node.js)会解析没有日期的日期,例如“ 2014 年 4 月”作为 2014 年 4 月 1 日,但 Firefox 返回 NaN。您必须通过适当的日期。 补充 Jason 的上述评论:如果您在 Firefox 中收到 NaN,另一个问题可能是 Firefox 和 Safari 不喜欢连字符日期。只有 Chrome 可以。请改用斜线。 【参考方案1】:

在第 5 版规范出来之前,Date.parse 方法完全依赖于实现new Date(string) 等效于 Date.parse(string),除了后者返回一个数字而不是 Date) .在第 5 版规范中,添加了要求以支持 simplified (and slightly incorrect) ISO-8601(另见 What are valid Date Time Strings in javascript?)。但除此之外,没有要求 Date.parse / new Date(string) 应该接受什么,除了他们必须接受任何 Date#toString 输出(不说那是什么)。

从 ECMAScript 2017(第 8 版)开始,实现需要解析 Date#toStringDate#toUTCString 的输出,但未指定这些字符串的格式。

从 ECMAScript 2019(第 9 版)开始,Date#toStringDate#toUTCString 的格式已被指定为(分别):

    ddd MMM DD YYYY HH:mm:ss ZZ [(时区名称)]例如2018 年 7 月 10 日星期二 18:39:58 GMT+0530 (IST) ddd, DD MMM YYYY HH:mm:ss Z例如格林威治标准时间 2018 年 7 月 10 日星期二 13:09:58

提供Date.parse 应该在新实现中可靠解析的另外 2 种格式(请注意,支持并不普遍,不兼容的实现将继续使用一段时间)。

我建议手动解析日期字符串并将Date constructor 与年、月和日参数一起使用以避免歧义:

// 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 由于某种原因无法使用英国日期格式 时间部分记录在@CMS 代码中。我使用此代码的日期格式为“2012-01-31 12:00:00”return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]); 完美运行,谢谢! @CMS 你说的implementationdependent是什么意思? @RoyiNamir,这意味着结果取决于运行您的代码的 Web 浏览器(或其他 JavaScript 实现)。 我也遇到了不同浏览器中新日期(字符串)行为不同的问题。它甚至不是在旧版本的 IE 上被破坏的问题,不同的浏览器只是不一致。永远不要使用 Date.parse 或 new Date(string)。【参考方案2】:

在最近编写 JS 解释器的经验中,我与 ECMA/JS 日期的内部工作作了很多斗争。所以,我想我会在这里投入我的 2 美分。希望分享这些内容可以帮助其他人解决有关浏览器在处理日期方面的差异的任何问题。

输入端

所有实现都将其日期值在内部存储为 64 位数字,表示自 1970-01-01 UTC 以来的毫秒数 (ms)(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 (ed 6) 中列出的仅有的两种格式之一,所有实现都必须以相同的方式进行解析(另一种是为 Date.prototype.toString 指定的格式)。

但是,即使对于 ISO 8601 格式字符串,某些实现也会出错。这是 Chrome 和 Firefox 的比较输出,当这个答案最初是在我的机器上使用 ISO 8601 格式字符串为 1970 年 1 月 1 日(纪元)编写的时所有实现:

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 日期。

因此,要直接回答原始问题,ECMA-262 要求 "YYYY-MM-DD" 被解释为 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:mm 或“Z”)。如果这样做,您可以安全地在本地时间和 UTC 时间之间来回切换。

这适用于浏览器(IE9 之后):

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

大多数当前浏览器确实平等对待其他输入格式,包括常用的 '1/1/1970' (M/D/YYYY) 和 '1/1/1970 00:00:00 AM' (M/D /YYYY hh: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 函数及其输出内容。请注意 toUTCStringtoISOString 函数在我的机器上输出 5:00 AM。此外,时区名称可能是缩写,并且在不同的实现中可能不同。

在打印前从 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"

我希望这个答案有帮助。

【讨论】:

首先,这是一篇精彩的文章。然而,我想指出一个依赖关系。关于时区说明符,您说:“没有说明符应该假定本地时间输入。”值得庆幸的是,ECMA-262 标准消除了任何假设的需要。 It states: "缺席时区偏移的值为 "Z"。"因此,没有指定时区的日期/时间字符串被假定为 UTC 而不是本地时间。当然,与 JavaScript 的许多东西一样,实现之间似乎几乎没有一致意见。 ...包括最常用的“1/1/1970”和“1/1/1970 00:00:00 AM”格式。 — 最常用在哪里?那肯定不在我的国家。 @ulidtko - 抱歉,我在美国。哇...你就在基辅。我希望你和你的家人保持安全,那里的情况很快就会稳定下来。照顾好自己,祝一切顺利。 这里只是一个注释。这似乎不适用于 Safari 浏览器(即 ios 或 OSX)。那或者我还有其他问题。 @Daniel——幸运的是,ECMAScript 作者修复了他们在日期和时间表示中缺少时区的错误。现在没有时区的日期和时间字符串使用主机时区偏移量(即“本地”)。令人困惑的是,ISO 8601 仅日期形式是 treated as UTC(尽管从规范中不是特别清楚),而 ISO 8601 将它们视为本地形式,因此它们没有解决所有问题。【参考方案3】:

这种疯狂有一些方法。作为一般规则,如果浏览器可以将日期解释为 ISO-8601,它会。 “2005-07-08”属于这个阵营,所以它被解析为UTC。 "Jul 8, 2005" 不能,所以解析为当地时间。

请参阅JavaScript and Dates, What a Mess! 了解更多信息。

【讨论】:

"作为一般规则,如果浏览器可以将日期解释为 ISO-8601,它就会。" 不受支持。 “2020-03-20 13:30:30”被许多浏览器视为 ISO 8601 和本地,但 Safari 的日期无效。大多数浏览器不支持许多 ISO 8601 格式,例如2004-W53-7 和 2020-092。【参考方案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】:

使用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 也可能给出不正确的结果。

【讨论】:

【参考方案6】:

根据http://blog.dygraphs.com/2012/03/javascript-and-dates-what-mess.html,格式“yyyy/mm/dd”解决了通常的问题。 他说:“尽可能坚持使用“YYYY/MM/DD”作为日期字符串。它得到普遍支持且明确无误。使用这种格式,所有时间都是本地时间。” 我已经设置了测试:http://jsfiddle.net/jlanus/ND2Qg/432/ 这种格式: + 通过使用 y m d 排序和 4 位数年份避免了日期和月份顺序的歧义 + 使用斜杠避免了 UTC 与本地不符合 ISO 格式的问题 + danvk,dygraphs 的家伙,说这种格式在所有浏览器中都很好。

【讨论】:

你可能想看看the author's answer。 如果您使用 jQuery,因为它使用 datepicker 解析器,我会说 jsFiddle 中示例的解决方案足够好。就我而言,问题出在 jqGrid 上,但发现它有它的 parseDate 方法。但无论如何,这个例子帮助了我,给了我一个想法,所以 +1,谢谢。 关于 dygraphs 的那篇文章是错误的,页面上的第一个示例清楚地说明了为什么使用斜杠而不是连字符是非常糟糕的建议。在撰写本文时,使用“2012/03/13”导致浏览器将其解析为本地日期,而不是 UTC。 ECMAScript 规范定义了对使用“YYYY-MM-DD”(ISO8601)的明确支持,因此请始终使用连字符。应该注意的是,在我写这篇评论的时候,Chrome 已经被修补以将斜杠视为 UTC。【参考方案7】:

虽然 CMS is correct 将字符串传递给 parse 方法通常是不安全的,但第 15.9.4.2 节中的新 ECMA-262 5th Edition(又名 ES5)规范建议 Date.parse() 实际上应该处理 ISO 格式的日期。旧规范没有提出这样的要求。当然,老的浏览器和一些现在的浏览器仍然不提供这个 ES5 功能。

你的第二个例子没有错。它是 UTC 中的指定日期,正如 Date.prototype.toISOString() 所暗示的那样,但以您当地的时区表示。

【讨论】:

并且在 ECMAScript 2015 中再次更改了日期字符串的解析,因此“2005-07-08”是本地的,而不是 UTC。顺便说一句,ES5 直到 2011 年 6 月才成为标准(目前 ECMAScript 2015 是)。 ;-) 为了混淆视听,TC39 于 10 月(在我上一篇文章之后的一个月)决定“2005-07-08”should be UTC,但是“2005-07-08T00:00:00”应该是本地的。两者都是符合 ISO 8601 的格式,都没有时区,但处理方式不同。去看看。【参考方案8】:

这个light weight date parsing library 应该可以解决所有类似的问题。我喜欢这个库,因为它很容易扩展。也可以 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") );

【讨论】:

找不到这个库 code google 上有存档,不过好像也是一样的:github.com/flavorjones/flexible-js-formatting【参考方案9】:

这是一个简短、灵活的 sn-p,用于以跨浏览器安全的方式转换日期时间字符串,@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) 如果将字符串拆分为组件部分,那么最可靠的下一步是将这些部分用作 Date 构造函数的参数。创建另一个字符串以提供给解析器只会让您回到步骤 1。“2014-04-29 13:00:15”应该被解析为本地,您的代码将其重新格式化为 UTC。 :-(【参考方案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 format 比较了日期,因此您可以直观地比较本地日期和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,即simple yet powerful:

// 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】:

accepted answer from 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

【讨论】:

以上是关于为啥 Date.parse 给出不正确的结果?的主要内容,如果未能解决你的问题,请参考以下文章

使用 sysdate 时的 Oracle SQL,为啥它没有给出预期的结果

当它的目标应该被删除时,为啥这个智能指针会给出正确的结果?

为啥 C# 'is' 运算符在比较两个布尔值时会给出正确的结果,我应该使用它吗?

梯度下降和正规方程没有给出相同的结果,为啥?

Date.parse和new Date(str)的兼容性问题

js Date.parse()兼容性问题