关于timezone:使用moment.js将一系列跨越Fall DST边界的非UTC时间戳转换为UTC时间戳的正确方法?

Correct way to use moment.js to convert a series of non-UTC timestamps that cross the Fall DST boundary to UTC timestamps?

背景

我正在为一个项目调查不同的moment.js用例,但是在夏时制问题上遇到了困难,最终在秋季结束。在问我的问题之前,因为我想清楚地为其他有类似问题的人提供背景,让我解释一下我在做什么,以及在春季夏令时发现了什么。

首先,我正在使用UTC时间戳和美国/纽约时间戳。在美国,2017年的夏令时从3月12日凌晨2点开始(从凌晨2:00:00到凌晨3:00:00),到11月5日凌晨2点结束(从凌晨2:00:00恢复到凌晨1:00:00)。因为我也知道我需要转换到的目标时区(美国/纽约),所以我不会依赖moment.js来检测我的本地时区,而是显式指定我想要的时区。

在夏时制生效的春天,观察夏时制的时区,如美国/纽约,向前跳了一个小时。moment.js处理得很好。

例如,如果我在美国/纽约夏令时生效前一秒钟通过了一个UTC时间戳,则如下所示:

1
moment('2017-03-12T06:59:59Z').tz('America/New_York').format('YYYY/MM/DD hh:mm:ss a z')

由于我的时间戳上的Z,上面的输入被视为UTC,我用.tz('America/New_York')显式地设置目标时区,以便它不使用本地系统时间。

或者,使用矩时区,我可以显式地将输入时区设置为UTC,并将输出设置为美国/纽约。

1
moment.tz('2017-03-12T06:59:59', 'UTC').tz('America/New_York').format('YYYY/MM/DD hh:mm:ss a z')

不管怎样,结果都是2017/03/12 01:59:59 am EST

然后,我在一秒钟后运行相同的命令。我将使用上面第一个示例中给出的格式,其中我将时间指定为UTC,然后将其转换为美国/纽约时间:

1
moment('2017-03-12T07:00:00Z').tz('America/New_York').format('YYYY/MM/DD hh:mm:ss a z')

我的结果和预期一样是正确的:2017/03/12 03:00:00 am EDT——由于日光节约,提前一个小时的时间被跳过了。

然后,我可以通过输入美国/纽约时间戳并将其转换为UTC,使用Moment时区返回到另一个方向。

1
moment.tz('2017-03-12T01:59:59', 'America/New_York').utc().format('YYYY/MM/DD hh:mm:ss a z')

这给了我1[4]

下一刻在美国/纽约时区,因为白天的节约开始发挥作用,是03:00:00,所以我把它转换成UTC…

1
    moment.tz('2017-03-12T03:00:00', 'America/New_York').utc().format('YYYY/MM/DD hh:mm:ss a z')

…然后得到看起来正确的2017/03/12 07:00:00 am UTC。美国/纽约时间跳过了一个小时("丢失"),但Moment可以检测到这一点并将其转换为UTC。

总之,对于美国的春季夏令时变化,我可以将UTC时间戳传递到moment.js或moment time zone中,并在另一个时区中返回时间戳,同时应用正确的夏令时偏移量。然后我还可以将美国/纽约时间戳传递给Moment,并返回正确转换的UTC时间戳。

我的问题

太好了,所以我想在秋季夏令结束的时候做同样的事情,当然也不是那么简单。我的假设是,由于日光节约有效地导致了一个小时的"重复",暂时无法知道正确的时间UTC。换言之,当夏令时有一个小时的间隔,现在我们有一个小时的重叠。

问题(第1部分):是否有一种方法可以将相对时间戳和时区传递到Moment中并返回正确的UTC时间?当我在下面的例子中尝试这个时,moment跳过了UTC方面的一个小时。

1
2
3
4
moment.tz('2017-11-05T01:00:00', 'America/Denver').tz('UTC').format('YYYY/MM/DD hh:mm:ss a z') =>"2017/11/05 07:00:00 am UTC"
moment.tz('2017-11-05T01:59:59', 'America/Denver').tz('UTC').format('YYYY/MM/DD hh:mm:ss a z') =>"2017/11/05 07:59:59 am UTC"
moment.tz('2017-11-05T02:00:00', 'America/Denver').tz('UTC').format('YYYY/MM/DD hh:mm:ss a z') =>"2017/11/05 09:00:00 am UTC"
moment.tz('2017-11-05T02:59:59', 'America/Denver').tz('UTC').format('YYYY/MM/DD hh:mm:ss a z') =>"2017/11/05 09:59:59 am UTC"

我假设这是因为时间是按时间顺序发生的,如下面的例子所示。未给出UTC偏移上下文,因此我假设时刻不能区分美国/纽约时间戳前面带星号:

1
2
3
4
5
6
*2017/11/05 01:00:00 am America/New_York => 2017/11/05 07:00:00 am UTC
*2017/11/05 01:59:59 am America/New_York => 2017/11/05 07:59:59 am UTC
*2017/11/05 01:00:00 am America/New_York => ???
*2017/11/05 01:59:59 am America/New_York => ???
2017/11/05 02:00:00 am America/New_York => 2017/11/05 09:00:00 am UTC
2017/11/05 02:59:59 am America/New_York => 2017/11/05 09:59:59 am UTC

再说一次,我想知道是否有办法解决这个问题?当前我所拥有的数据中的时间戳不包含UTC偏移量。

问题(第2部分):如果我在美国/纽约时区展示数据,那么我是否认为我基本上将有两个小时的数据点,所有这些数据点都填充到(似乎)2017年11月5日01:00:00至01:59:59的一个小时周期中?

相关主题

还有其他一些与此相关的主题,但没有一个与此相关的主题是我发现的。我将在这里链接几个以供参考:

  • moment.js将本地时间转换为UTC时间确实有效
  • 用我创建的时区偏移量初始化一个时刻

看来你已经把这个问题想清楚了,做了一些基础研究。谢谢!

您所描述的内容包含在这里的即时时区文档中。如果数据在输入中不可用作UTC偏移量,则无法区分不明确本地时间的第一次或第二次出现之间的差异。因为时间是向前移动的,所以对于大多数场景来说,这通常是最明智的选择。

问题是模棱两可。即使只是一个普通人,如果我说"2017年11月5日上午1:00在纽约",你也不知道我在描述两个时间点中的哪一个。

也就是说,有时候你有外部知识可以帮助你。例如,如果您有一组有序的时间戳,其中包含向后跳过的时间,那么您就知道遇到了向后回退转换。假设我在本地时间每隔15分钟记录一次数据:

1
2
3
4
5
6
7
8
9
10
00:45
01:00
01:15
01:30
01:45
01:00  <---  this one comes next sequentially, but appears backwards, so infer transition
01:15
01:30
01:45
02:00

对于这个场景,您必须编写自己的检测逻辑来将一个值与另一个值进行比较。另外请注意,如果您没有任何时间出现顺序错误,那么您就不能确定描述的是哪个事件。在某些情况下,"心跳"信号可以帮助实现这一点。

现在,在不预先知道偏移量的情况下,如何选择瞬间的第二次出现?这样地:

首先,从这里获取hasAmbiguousWallTime函数。

然后定义另一个函数:

1
2
3
4
5
function adjustToLaterWhenAmbiguous(m) {
    if (hasAmbiguousWallTime(m)) {
        m.utcOffset(moment(m).add(1, 'hour').utcOffset(), true);
    }
}

现在您可以这样做:

1
2
3
4
5
6
7
8
9
// start with the first occurrence
var m = moment.tz("2017-11-05T01:00:00","America/New_York");
m.format(); //"2017-11-05T01:00:00-04:00"

// now shift it to the second occurrence
if (... your logic, such as wall time going backwards in sequence, etc. ...) {
    adjustToLaterWhenAmbiguous(m);
    m.format(); //"2017-11-05T01:00:00-05:00"
}

这两个功能可能需要加强并添加到Moment时区,但是对于您描述的场景来说,它们应该足够了。

其他几个要点:

  • 代替moment.tz(s, 'UTC'),考虑使用moment.utc(s)
  • 考虑使用moment.tz(s, 'America/New_York').utc()
  • 您可能需要查看DST标记wiki以可视化问题空间。
  • 在你的问题的第二部分,是的——你最终会把两个小时的数据塞进一个小时的空间中。人们总是对图表有这个问题。他们用一个不变的当地时间来绘制一些图,然后在春天看到一个零效应,在秋天看到一个加倍效应。即使告诉Moment使用后一个事件,也不会避免这种情况,除非您实际以UTC而不是本地时间显示图表。