消除 Javascript 夏令时差距,一种跨浏览器的解决方案

Eliminating Javascript daylight saving time gap, a cross-browser solution

经过一番折腾,我终于找到了真正的问题所在。这是由夏令时引起的差距,如果时区设置为 UTC+3:30(我不确定其他时区),不同浏览器的行为会有所不同。

这是产生问题的一个片段(如果您的系统的 TZ 设置为 UTC+3:30,问题是可重现的):

function zeroPad(n) {
  n = n + '';
  return n.length >= 2 ? n : new Array(2 - n.length + 1).join('0') + n;
}

document.write("<table border='1' cellpadding='3'><tr><td>Input date</td><td>Parsed timestamp</td><td>Output date</td></tr>");

var m = 22 * 60;
for (var i=0; i<8; i++) {
  var input = "3/21/2015 " + zeroPad(Math.floor(m / 60)) + ":" + zeroPad(m % 60) + ":00";
  var d = new Date(input);
  var output = d.getFullYear()
    +'-'+zeroPad(d.getMonth()+1)
    +'-'+zeroPad(d.getDate())
    +' '+zeroPad(d.getHours())
    +':'+zeroPad(d.getMinutes())
    +':'+zeroPad(d.getSeconds());
  
  
  document.write("<tr><td>" + input + "</td><td>" + d.getTime() + "</td><td>" + output + "</td></tr>");
  m = m + 15;
}
m = 0;
for (var i=0; i<7; i++) {
  var input = "3/22/2015 " + zeroPad(Math.floor(m / 60)) + ":" + zeroPad(m % 60) + ":00";
  var d = new Date(input);
  var output = d.getFullYear()
    +'-'+zeroPad(d.getMonth()+1)
    +'-'+zeroPad(d.getDate())
    +' '+zeroPad(d.getHours())
    +':'+zeroPad(d.getMinutes())
    +':'+zeroPad(d.getSeconds());
  
  
  document.write("<tr><td>" + input + "</td><td>" + d.getTime() + "</td><td>" + output + "</td></tr>");
  m = m + 15;
}

document.write("</table>");

我在 Firefox 和 Chromium 上 运行 它是这样的,他们是这样说的:

红框内的部分为gap发生的时间范围。我的问题是像日历这样的组件通常依赖于时间部分设置为“00:00:00”的日期对象,并且它们有一个循环,通过在前一个日期上添加一天的时间戳来生成新日期。因此,一旦对象落入 3/22/2015 00:00:00,它将被视为 3/22/2015 01:00:0021/3/2015 23:00:00(取决于浏览器),因此生成的日期将从那个时间点开始无效!

问题是如何检测此类日期对象以及如何处理它们?

您必须使用变长参数构造函数。

function convert(dateStr) {
  var date = dateStr.split('/');
  return new Date(date[2], (date[0] | 0) - 1, date[1]);
}
                  
function formatDate(d) {
  function pad(i) {
    return i < 10 ? '0' + i : i;
  }
  return d.getFullYear()
       + '-' + pad(d.getMonth() + 1)
       + '-' + pad(d.getDate())
       + ' ' + pad(d.getHours()) 
       + ':' + pad(d.getMinutes()) 
       + ':' + pad(d.getSeconds())
}
  
var str = formatDate(convert('3/22/2015'));

document.write(str);

使用 moment.js 会让您省去很多麻烦,并且是实现此类事情的跨浏览器兼容性的最简单方法。

var m = moment.utc("3/22/2015","M/D/YYYY")
var s = m.format("YYYY-MM-DD HH:mm:ss")

为此使用 UTC 很重要,因为您不希望受到用户时区的影响。否则,如果您的日期进入 DST 转换,它可能会调整为其他值。 (您并不是 真正 对 UTC 感兴趣,您只是为了稳定使用它。)


针对您更新问题的这一部分:

To simplify the question, I'm looking for a function like this:

function date_decomposition(d) {
    ...
}

console.log(date_decomposition(new Date("3/22/2015 00:00:00")));
=> [2015, 3, 22, 0, 0, 0]

虽然现在很清楚您的要求,但您必须了解 不可能 达到您的确切要求,至少在跨浏览器、跨区域中是这样, 跨时区方式。

  • 每个浏览器都有自己的方式来实现字符串到最新的解析。当您使用构造函数 new Date(string)Date.parse(string) 方法时,您调用的功能是 实现特定的 .

    There's a chart showing many of the formatting differences here.

  • 即使在所有环境中实现一致,您仍需要应对区域格式和时区差异。

    • 如果出现区域格式问题,请考虑 01/02/2015。一些地区使用 mm/dd/yyyy 排序并将其视为 1 月 2 日,而其他地区使用 dd/mm/yyyy 排序并将其视为 2 月 1 日。 (此外,世界上的某些地区使用 yyyy/mm/dd 格式。)

      Wikipedia has a list and map of where in the world different date formats are used.

    • 在时区的情况下,考虑October 19th, 2014 at Midnight (00:00) in Brazil did not exist, and November 2nd, 2014 at Midnight (00:00) in Cuba存在两次

      同样的事情发生在不同时区的其他日期和时间。根据你提供的信息,我可以推断你在伊朗时区,标准时间使用UTC+03:30,白天使用UTC+04:30。的确,March 22, 2105 at Midnight (00:00) did not exist in Iran.

      当您尝试解析这些无效或不明确的值时,每个浏览器都有自己的行为,并且浏览器之间确实存在差异。

      • 对于无效时间,有些浏览器会向前跳一个小时,而有些浏览器会向后跳一个小时。

      • 对于模棱两可的时间,一些浏览器会假定您指的是第一个(夏令时)实例,而其他浏览器会假定您指的是第二个(标准时间)实例。

综上所述,您当然可以拿一个 Date 对象 并解构它的各个部分,非常简单:

function date_decomposition(d) {
    return [d.getFullYear(), d.getMonth()+1, d.getDate(),
            d.getHours(), d.getMinutes(), d.getSeconds()];
}

但这将始终基于代码为运行的本地时区。您会看到,在 Date 对象内部,只有一个值 - 一个数字,表示自 1970-01-01T00:00:00Z 以来经过的毫秒数(不考虑闰秒)。该数字是基于 UTC 的。

因此,总而言之,您遇到的所有问题都与将字符串解析为 Date 对象的方式有关。再多关注输出函数也无法帮助您以完全安全的方式解决该问题。无论您使用库还是编写自己的代码,都需要获取原始数据字符串才能获得所需的结果。当它在 Date 对象中时,您已经丢失了完成这项工作所需的信息。

顺便说一下,您可以考虑观看我的 Pluralsight 课程 Date and Time Fundamentals,其中更详细地介绍了其中的大部分内容。第 7 单元完全是关于 JavaScript 和这些陷阱的。

日期解析(修改问题前的第一个答案)

这将解析输入并隔离每个值。

function dateDecomposition(d) {
    return [ d.getFullYear(),d.getMonth()+1,d.getDate(),
             d.getHours(),d.getMinutes(),d.getSeconds() ];
};
function dateUTCDecomposition(d) {
    return [ d.getUTCFullYear(),d.getUTCMonth()+1,d.getUTCDate(),
             d.getUTCHours(),d.getUTCMinutes(),d.getUTCSeconds() ];
};

function pdte() {
  var ndte=new Date(Date.parse(document.getElementById('datein').value));
  var ar=dateDecomposition(ndte);
  document.getElementById('yyyy').innerHTML=ar[0];
  document.getElementById('mm').innerHTML=ar[1];
  document.getElementById('dd').innerHTML=ar[2];
  document.getElementById('HH').innerHTML=ar[3];
  document.getElementById('MN').innerHTML=ar[4];
  document.getElementById('SS').innerHTML=ar[5];
  ar=dateUTCDecomposition(ndte);
  document.getElementById('Uyyyy').innerHTML=ar[0];
  document.getElementById('Umm').innerHTML=ar[1];
  document.getElementById('Udd').innerHTML=ar[2];
  document.getElementById('UHH').innerHTML=ar[3];
  document.getElementById('UMN').innerHTML=ar[4];
  document.getElementById('USS').innerHTML=ar[5];

  document.getElementById('test').innerHTML='';
  for (var i=1426896000000;i<1427068800000;i+=1800000) {
      ar=dateUTCDecomposition(new Date(i));
      var cmp=Date.parse(ar[0]+"/"+ar[1]+"/"+ar[2]+" "+ar[3]+":"+ar[4]+":"+ar[5]);
      var re=dateDecomposition(new Date(cmp));
      var fail=0;
      for (var l=0;l<6;l++) { if (ar[l]!==re[l]) fail=1 };
      document.getElementById('test').innerHTML+=fail+" -- "+ar.join(":")+'<br />';
  };
};

document.getElementById('datein').addEventListener('change',pdte,true);
window.onload=pdte
<input id="datein" value="2015/03/14 12:34:56" />
<table border=1>
  <tr><td>Locale</td>
    <td id="yyyy"></td>
    <td id="mm"></td>
    <td id="dd"></td>
    <td id="HH"></td>
    <td id="MN"></td>
    <td id="SS"></td>
  </tr><tr><td>UTC</td>
    <td id="Uyyyy"></td>
    <td id="Umm"></td>
    <td id="Udd"></td>
    <td id="UHH"></td>
    <td id="UMN"></td>
    <td id="USS"></td>
    </tr>
  </table>
  <div id="test"></div>

测试规则#1

正在搜索从 2000 年到 2015 年的 差距,以 1/2 小时为步长

function dateDecomposition(d) {
    return [ d.getFullYear(),d.getMonth()+1,d.getDate(),
             d.getHours(),d.getMinutes(),d.getSeconds() ];
};
function dateUTCDecomposition(d) {
    return [ d.getUTCFullYear(),d.getUTCMonth()+1,
             d.getUTCDate(), d.getUTCHours(),
             d.getUTCMinutes(),d.getUTCSeconds() ];
};

for (var i=946684800000;i<1420070400000;i+=1800000) {
      ar=dateUTCDecomposition(new Date(i));
      var cmp=Date.parse(
          ar[0]+"/"+ar[1]+"/"+ar[2]+" "+
          ar[3]+":"+ar[4]+":"+ar[5]);
      var re=dateDecomposition(new Date(cmp));
      var fail=0;
      for (var l=0;l<6;l++) { if (ar[l]!==re[l]) fail=1 };
    if (fail!==0) {
 document.getElementById('test').innerHTML+=fail+
          " -- "+new Date(i)+" -- "+ar.join(":")+'<br />';
    }
}
div#test {
  font-family: mono, monospace, terminal;
  font-size: .8em;
}
   <div id="test"> </div>

总之,这似乎不是 javascript 错误,但我在 ExtJs 中发现了这个: Ext.Date.parse - problem when parsing dates near daylight time change.

第二个测试规则

由于这个问题被修改了几次,有一个测试规则在过去 10 年内有效,以 15 分钟为步长,用于显示 gaps AND overlaps:

function dateDecomposition(d) {
    return [ d.getFullYear(),d.getMonth()+1,d.getDate(),
             d.getHours(),d.getMinutes(),d.getSeconds() ];
};
function dateUTCDecomposition(d) {
    return [ d.getUTCFullYear(),d.getUTCMonth()+1,
             d.getUTCDate(), d.getUTCHours(),
             d.getUTCMinutes(),d.getUTCSeconds() ];
};

var end=(Date.now()/900000).toFixed(0)*900000;
var start=end-365250*86400*10;
for (var i=start;i<end;i+=900000) {
  ar=dateUTCDecomposition(new Date(i));
  var cmp=Date.parse(
      ar[0]+"/"+ar[1]+"/"+ar[2]+" "+
      ar[3]+":"+ar[4]+":"+ar[5]);
  var re=dateDecomposition(new Date(cmp));
  var fail=0;
  for (var l=0;l<6;l++) { if (ar[l]!==re[l]) fail++ };
  if (fail!==0) {
    document.getElementById('test').innerHTML+=
      fail+" -- "+new Date(i)+" -- "+ar.join(":")+'<br />';
  } else {
    ar=dateDecomposition(new Date(i));
    var cmp=Date.parse(
          ar[0]+"/"+ar[1]+"/"+ar[2]+" "+
          ar[3]+":"+ar[4]+":"+ar[5]);
    if (cmp != i) {
      document.getElementById('test').innerHTML+=
      fail+" -- "+new Date(i)+" -- "+ar.join(":")+'<br />';
    }
  }
}
div#test {
  font-family: mono, monospace, terminal;
  font-size: .8em;
}
   <div id="test"> </div>

您可以通过 运行 您喜欢的浏览器在一些不同的时区下进行测试:

env TZ=Iran firefox 
env TZ=Iran opera 
env TZ=Iran chrome 
env TZ=Iran chromium 
env TZ=Europe/Berlin firefox 
env TZ=Europe/Berlin opera 
env TZ=Europe/Berlin chrome 
env TZ=US/Central firefox 
env TZ=US/Central opera 
env TZ=US/Central chrome 
env TZ=Asia/Gaza firefox 
env TZ=Asia/Gaza opera 
env TZ=Asia/Gaza chromium 

我希望您能够为您的 ExtJS 扩展 创建自己的测试规则,从最后一个片段中汲取灵感。

屏幕截图

TZ=伊朗铬

TZ=伊朗冰鼬 (firefox)

TZ=US/Central 铬

TZ=US/Central冰鼬

TZ=Asia/Gaza 铬

TZ=Asia/Gaza冰鼬

JavaScript 日期以浏览器的本地时间解析,除非您使用 ISO 日期格式 "2015-03-22",它将被解析为 UTC。在您的情况下,由于您控制服务器,并且您知道日期实际上是 UTC,因此您可以保持格式不变并解析日期,然后从日期中减去时区偏移值以将其转换为 UTC。以下函数应该 return 完全符合您在上一个用例中请求的结果。您显然可以修改它以按照您认为合适的任何方式格式化字符串。:

function date_decomposition(dateString) {
    var local = new Date(dateString);
    var d = new Date(local.valueOf() - (local.getTimezoneOffset() * 60000));

    return [ d.getUTCFullYear(), 
            (d.getUTCMonth() +1 ),
            d.getUTCDate(),
            d.getUTCHours(), 
            d.getUTCMinutes(),
            d.getUTCSeconds() ];
}

关于此方法的唯一问题是,如果您将服务器上的日期格式更改为使用 ISO 8601 日期格式,它仍会减去时区偏移量,并且您最终会得到错误的输出。

为了 100% 安全,您应该 使用 ISO 8601 日期格式格式化日期字符串,例如 "2015-03-22""2015-03-22T00:00:00Z"。然后你可以删除从解析日期中减去时区偏移量的行,它可以与任何 ISO 日期字符串一起正常工作:

function date_decomposition(dateString) {
    var d = new Date(dateString);

    return [ d.getUTCFullYear(), 
            (d.getUTCMonth() +1 ),
            d.getUTCDate(),
            d.getUTCHours(), 
            d.getUTCMinutes(),
            d.getUTCSeconds() ];
}

更多的是方法上的问题。浏览器的行为完全符合预期,因为它们都知道 Upcoming Daylight Saving Time Clock Changes。因此,您的 Chromium 浏览器知道在 3 月 22 日当地时间 12:00 午夜,需要将时钟向前移动到 1:00 AM,而您的 Firefox 浏览器知道在 3 月 22 日当地时间周六至周日午夜, 需要将时钟调回星期六下午 11:00。

根据 UTC 定义不同的时区是有原因的。 UTC 是一个标准,而不是时区。 如果你想跨浏览器,你需要遵守标准。坚持 UTC,存储在 UTC 中,并在 UTC 中进行所有日期计算。如果您有日历组件,请告诉您的组件您正在传递 UTC 日期。当您需要向用户显示日期(因此依赖于浏览器)时,您(如果您没有使用组件)或您的日历组件负责检查夏令时是否有效,并调整要显示的日期因此。

¿你怎么知道夏令时是否有效?

检查此 article about adding methods to the date object 以完全做到这一点。代码背后的逻辑是,无论夏令时是否生效,UTC 和本地时间之间的时区差异是不同的。如果你只是想检查这里的代码是两个函数:

Date.prototype.stdTimezoneOffset = function() {
    var jan = new Date(this.getFullYear(), 0, 1);
    var jul = new Date(this.getFullYear(), 6, 1);
    return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
}

Date.prototype.dst = function() {
    return this.getTimezoneOffset() < this.stdTimezoneOffset();
}

我找到了你的问题的解决方案

一定会帮到你

首先我想解释一下夏令时

日照时间:夏季日照时间较长;太阳升得早,落得晚。几十年前,国会决定晚上的日光比早上多,因此他们提出了夏令时。 spring 时钟向前移动一小时,秋季则向后移动一小时。这样,您就不会在 4:00 a.m 看到日出。日落在 8:00 p.m,但日出在 5:00 a.m。和日落 9:00 p.m,在盛夏。他们认为人们会在晚上用更少的电来照亮他们的房子,等等。 所以现在,该国大部分地区在夏季使用夏令时,在冬季使用标准时间。 yahoo link

首先我想给你一个问题的解决方案:

将您的计算机(在 win 7 上测试)的时区设置为 GMT+3:30,并将您的日期更改为 2015 年 3 月 23 日。 和 运行 您在两种浏览器上的代码 (chromium/mozila)。 您会发现相同的结果(即输入时间增加 1 小时)

是的,问题解决了。

您的输入时间将增加 +1 小时,因为在 2015 年 3 月 22 日 12:00 AM(格林威治标准时间 +3:30)之后,夏令时开始。 (you can check it here also from here here)

现在的原因是:

请查看此屏幕截图:http://prntscr.com/6j4gdn(在 win 7 上) 在这里你可以看到“夏令时开始于 2015 年 3 月 22 日 12:00 AM。

表示时间将在 2015 年 3 月 22 日上午 12:00 后提前 1 小时。(仅适用于那些使用夏令时系统的国家)

因此,如果您将时间更改为 2015 年 3 月 23 日,则夏令时开始于(例如 GMT+3:30) 你会发现在两种浏览器上的输出都是正确的。

我猜chrome总是使用夏令时系统(仅适用于那些使用夏令时系统的国家) 我已经通过更改系统时间对其进行了测试(但不是 100% 确定)

另一方面,mozila 支持夏令时系统。因此,在 2015 年 3 月 22 日 12:00 AM(格林威治标准时间+3:30)之后,mozila 将为您输入的日期时间增加 1 小时

我想这对你们所有人都有帮助。