使用 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 时间戳的正确方法?的主要内容,如果未能解决你的问题,请参考以下文章