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

Posted

技术标签:

【中文标题】使用 moment.js 将跨越秋季 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? 【发布时间】:2017-09-07 16:29:09 【问题描述】:

背景

我正在为一个项目调查 moment.js 的不同用例,但对秋季结束的夏令时问题感到困惑。在问我的问题之前,由于我想澄清并为有类似问题的其他人提供背景,让我解释一下我在做什么以及春季夏令时的发现。

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

在夏令时生效的春季,实行夏令时的时区(例如 America/New_York)提前一个小时。 moment.js 处理得很好。

例如,如果我在夏令时对 America/New_York 生效前一秒向 moment.js 传递 UTC 时间戳,它看起来像这样:

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,并将输出设置为 America/New_York。

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,然后将其转换为 America/New_York 时间:

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 - 由于夏令时,时间提前了一小时。

然后我可以通过传入 America/New_York 时间戳并将其转换为 UTC 来使用 moment-timezone 以另一种方式返回。

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

这给了我2017/03/12 06:59:59 am UTC

在 America/New_York 时区的下一个时刻是 03:00:00,因此我将其转换为 UTC...

    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。 在美国/纽约时间跳过(“丢失”)一个小时,但时刻可以检测到这一点并将其转换为 UTC。

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

我的问题

太好了,所以我想在秋季夏令时结束时做同样的事情,当然这不是那么简单。我的假设是,由于夏令时有效地导致一个小时“重复”,因此暂时无法知道正确的 UTC 时间。换句话说,夏令时开始时有 1 小时的间隔,现在我们有 1 小时的重叠。

问题(第 1 部分):有没有办法将相对时间戳和时区传递到时刻并取回正确的 UTC 时间?当我在下面的示例中尝试此操作时,UTC 方面的时刻会跳过一个小时。

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 偏移上下文,因此我假设时刻无法区分以星号开头的 America/New_York 时间戳:

*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 部分):如果我在 America/New_York 时区呈现数据,那么认为我将基本上将两个小时的数据点全部塞入从 01 开始的(看似)单个一小时的时间段中是否正确2017 年 11 月 5 日 :00:00 到 01:59:59?

相关主题

关于 SO 的其他一些主题与此相关,但我发现没有一个主题构成或回答相同的问题。我将在这里链接一些以供参考:

Moment.js Convert Local time to UTC time does work Initialize a Moment with the timezone offset that I created it with

【问题讨论】:

【参考方案1】:

看来您已经仔细考虑了问题并进行了一些基础研究。谢谢!

您所描述的是covered in the moment-timezone docs here。如果数据在您的输入中不能用作 UTC 偏移量,则无法区分第一次或第二次出现不明确的本地时间之间的差异。 Moment 选择第一个出现,因为时间向前移动,所以这通常是大多数情况下最明智的选择。

问题在于模棱两可。即使作为一个普通人,如果我说“2017 年 11 月 5 日凌晨 1 点,纽约”,你也不知道我描述的是两个时间点中的哪一个。

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

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

对于该场景,您必须编写自己的检测逻辑来将一个值与下一个值进行比较。另请注意,如果您没有任何时间出现乱序,那么您无法确定所描述的是哪个事件。在某些情况下,“心跳”信号可以帮助解决此问题。

现在你如何在事先不知道偏移量的情况下选择 Moment 中的第二次出现?像这样:

首先,获取hasAmbiguousWallTime函数from here。

然后定义另一个函数:

function adjustToLaterWhenAmbiguous(m) 
    if (hasAmbiguousWallTime(m)) 
        m.utcOffset(moment(m).add(1, 'hour').utcOffset(), true);
    

现在你可以这样做了:

// 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.utc(s),而不是moment.tz(s, 'UTC') 考虑使用moment.tz(s, 'America/New_York').tz('UTC') 而不是 使用moment.tz(s, 'America/New_York').utc() 您可能需要查看DST tag wiki 以了解问题空间的可视化。 在您的问题的第二部分,是的 - 您最终会将两小时的数据填充到可能被可视化为一小时的空间中。人们一直对图形和图表有这个问题。他们在本地时间绘制具有恒定值的东西,然后在春季看到归零效应,在秋季看到加倍效应。即使您告诉 moment 使用稍后出现的时间,除非您实际以 UTC 而不是本地时间显示图表,否则您将无法避免这种情况。

【讨论】:

非常感谢马特,这非常有帮助!我也没有意识到堆栈交换中有标记 wiki - 感谢您指出这一点。直观地看到问题是一个巨大的帮助。我也会按照建议使用moment.utc()

以上是关于使用 moment.js 将跨越秋季 DST 边界的一系列非 UTC 时间戳转换为 UTC 时间戳的正确方法?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 fullcalendar 处理 DST 显示混乱

Moment.js 没有显示正确的时间

哪个 JS 日期时间选择器正确处理 DST 转换?

将 moment.js 与 vue.js 一起使用

在python中获取给定时区的DST边界

如何将 Moment.js 日期转换为用户本地时区?