针对重复发生的事件,使用moment.js防止DST偏移

preventing DST shift with moment.js for recurring events

本文关键字:moment 使用 js 防止 偏移 DST 事件      更新时间:2023-09-26

我正在构建的系统有一个事件组件,其中一部分是创建重复事件的能力。在我的数据库中,我将所有事件存储在UTC中。当一个定期事件显示在用户的日历上时,它应该始终以"墙时间"显示。因此,例如,如果我在每周三下午1:00创建一个定期活动,无论夏令时如何,它都应该始终在下午1:00。

我遇到的问题是,每当我试图使用Moment.js格式化这个日期时,Moment总是会考虑夏令时的变化,并相应地更新事件。以我之前的例子为例,在2016年,夏令时轮班发生在3月13日,因此,如果我的约会是在2016年2月预订的,那么直到3月13号的每个约会都会正确输出为下午1:00。3月13日之后,夏令时开始实施,我的所有活动现在都提前了一个小时。

有推荐的处理方法吗?我似乎找不到让Moment"忽略"DST的方法,在搜索时也找不到合适的解决方案。

不知道这有多重要,但我在输出日期时使用Moment时区转换为用户的本地时区。

谢谢!

根据最佳实践,未来的日期应存储在当地时间。这是因为,正如评论中所指出的,时区规则可以而且确实会改变。因此,您无法提前正确计算当地时间的UTC日期。

当你存储未来的日期时,你应该将它与用户期望事件时间所在的IANA时区一起存储。然后你实际上会做与现在相反的转换。如果您需要知道事件在全球时间线上的确切点,您可以将当地时间转换为UTC。

请注意,IANA时区数据库经常更新。它现在是2016d版本,这意味着它今年已经更新了四次。如果你的应用程序将在多个国家/地区使用,你需要非常勤奋地不断更新时刻时区以跟上变化。

至于为什么你现在看到你的UTC日期没有正确更改,我认为你是在计算你的计划日期,转换为UTC,然后计算你的重复时间。当你用UTC计算日期时,它不会正确解释夏令时的变化,因为它不知道这些变化。因此,当moment将日期转换回本地时,它将延迟一个小时。如果你用当地时间计算日期,然后转换为UTC进行存储,那么只要时区规则没有改变,你的转换就正确了。不过,不要这样做——用当地时间存储日期。

替代(部分)解决方案

还有一种解决夏令时轮班问题的替代方案注意:当某些时区的时区偏移量将来发生变化时,这将无法按预期工作它不使用moment.js,但我认为它仍然可能对某人有所帮助。

解释

诀窍是在创建实体时使用当前UTC到本地时间的偏移量来调整UTC日期,然后存储更新后的UTC日期。

假设本地时区是'Europe/Berlin',我们正在创建一个从'2020-03-28 10:00:00Z'(UTC)开始的重复事件。这是我们的参考日期。此时时区的偏移量为+01:00,因此用户的意图是让事件在11:00开始。

现在,此事件一直重复到'2020-03-29'(即该时区夏令时偏移后的第二天,该时区的偏移量为+02:00)。因此,如果我们不更改任何内容,事件将在当天的12:00显示(而不是按照用户的意愿显示11:00)。

因此,如果我们将日期存储为当天的'2020-03-29 09:00:00Z'(UTC),则它将正确显示在当地时区的11:00。

代码

这有点棘手,我们应用时区偏移的部分肯定会得到改进,但对于我们的用例来说,这已经足够了。它在Node.js环境中使用date-fns和date-fns时区。

  • referenceDate:UTC日期,用作要应用的偏移的参考(例如,系列的第一个日期;上例中的'2020-03-28 10:00:00Z'
  • date:当前重复的UTC日期;上例中的'2020-03-29 10:00:00Z'
  • timeZone:本地时区的IANA时区名称;上例中的'Europe/Berlin'
  • adjustedUTCDate:调整后的UTC日期

转换代码:

// offset == '+01:00' in the example
let offset = dateFnsTimezone.formatToTimeZone(referenceDate, 'Z', { timeZone });
if (offset[0] === '+') {
    offset = offset.replace('+', '-');
} else {
    offset = offset.replace('-', '+');
}
const dateString = dateFns.format(date, 'YYYY-MM-DDTHH:mm:ss[Z]');
// applies the offset to change the time (intended local time); '2020-03-29 10:00:00+01:00' in the example
const localDate = dateString.replace('Z', offset);
// convert the date back to UTC, from the intended local time; '2020-03-29 09:00:00' in the example
const adjustedUTCDate = dateFnsTimezone.parseFromTimeZone(localDate, { timeZone });