Rrule: UTC-but-not-UTC 混淆

创建于 2019-04-05  ·  18评论  ·  资料来源: jakubroztocil/rrule

我怀疑这不是 rrule 的问题,而是我对它和/或其时区支持的理解。 我已经阅读了rruleluxon文档中的相关部分。

代码示例

const { RRule, RRuleSet } = require(`rrule`);

const dtstart = new Date();
console.log(`dtstart`, dtstart);

const firstHour = dtstart.getHours();
const secondHour = (firstHour + 2) % 24;

const getRRule = options => new RRule(Object.assign({}, options, {
    freq: RRule.DAILY,
    count: 1,
    dtstart,
    byminute: 0,
    bysecond: 0
}));

const showNextInstance = tzid => {
    const ruleSet = new RRuleSet();
    ruleSet.rrule(getRRule({ tzid, byhour: firstHour }));
    ruleSet.rrule(getRRule({ tzid, byhour: secondHour }));
    console.log(tzid, ruleSet.after(dtstart));
};

showNextInstance(undefined);
showNextInstance(`UTC`);
showNextInstance(`local`);
showNextInstance(`America/Chicago`);

实际产量

dtstart 2019-04-05T11:51:23.744Z
undefined 2019-04-06T06:00:00.744Z
UTC 2019-04-06T06:00:00.744Z
local 2019-04-06T06:00:00.744Z
America/Chicago 2019-04-06T06:00:00.744Z

预期输出

我认为tzid的规范会对输出产生一些影响,但每个时区都会生成相同的输出。

我也看不到规则中提供的时间与下一个事件实例的输出之间的相关性。 我希望下一个实例大致对应于提供的规则表示的两个小时之一,具体取决于指定的dtstarttzid值的解释或应用方式。

规则版本

2.6.0

操作系统

OS X 10.14.2,节点 8.11.4

当地时区

$ date
Fri Apr  5 06:32:33 CDT 2019

最有用的评论

我认为很容易将Date对象的问题和RRULE计算出现的问题混为一谈,所以我将在将两者结合之前分别讨论它们。 我不得不把这个主要写出来,这样我才能理清我对这个问题的理解,并想我会在这里发帖,以防它对其他人有帮助。

JavaScript 日期对象

我不同意 JS Date对象是不规则的,但是与对象之间存在常见的误解,这确实使它看起来如此。 至少可以说是令人困惑的。

正如@hlee5zebra 所提到的,使用Date构造函数创建的日期始终以 UTC 表示。 它们可能以当地时间显示,但在幕后,它们实际上以 Unix 纪元时间表示。 我们总是可以在实例化的Date对象上运行.getTime().toISOString()来提醒自己我们在 UTC 下工作。

该实现还正确地遵循了以 UTC 进行所有处理并且仅转换为本地时间显示给用户的约定。 碰巧我们,开发者,是用户; 但是我们有自己的用户,这让水变得浑浊。 在开发我们的应用程序时,我们必须确保遵循约定并将日期保存为 UTC 在我们的后端(JS 已经以 UTC 处理日期,所以我们在那里很好)。 如果您将日期作为 UTC 存储在您的数据存储中,它们应该作为ISO 字符串到达客户端,由Date对象正确解析,并以用户的本地时间显示,所有这些都无需做任何繁重的工作。

这篇文章帮助我理解了Date对象并发现了我可以用它做什么: https :

规则

计算RRULE出现次数变得棘手,因为用户期望和 UTC 中的日期处理存在根本差异,当这两个时间表示跨越一天的障碍时,这一点很明显。

例如,如果我们的用户希望每个星期二发生一次,他们就会期望所有事件都发生在他们当地时间的星期二。 但是,所有计算都是在 UTC 中完成的,所以RRULE像:

DTSTART:2019-10-29T00:02:26Z
RRULE:FREQ=WEEKLY;BYWEEKDAY=TU

将在 UTC 返回星期二发生的事件,但对于位于本初子午线以西的任何用户,事件将出现在当地时间的星期一,这显然不是任何人希望的结果。

做对了

综上所述,开发人员必须做的就是强制 JavaScript 认为本地日期实际上是 UTC 日期,将“假”日期赋予rrule.js ,并让库在此本地 UTC 时间。 当您从.all()方法中获取事件时,您可以使用这些日期的“UTC”部分实例化新的Date对象。 这有效地使结果符合预期。

import { RRule } from 'rrule'

const dtstart = new Date('2019-10-29T00:02:26Z') // same as: `new Date(Date.UTC(2019, 9, 29, 0, 2, 26))`
console.log(dtstart.toISOString()) // 2019-10-29T00:02:26.000Z
console.log(dtstart) // Mon Oct 28 2019 17:02:26 GMT-0700 (Pacific Daylight Time)

const fakeDateStart = setPartsToUTCDate(dtstart)
console.log(fakeDateStart.toISOString()) // 2019-10-28T17:02:26.000Z
console.log(fakeDateStart) // Mon Oct 28 2019 10:02:26 GMT-0700 (Pacific Daylight Time)

const rule = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.TU],
  count: 2,
  dtstart: fakeDateStart
})

const localUTCOccurrences = rule.all()
console.log(localUTCOccurrences.map(toISOString)) // ["2019-10-29T17:02:26.000Z", "2019-11-05T17:02:26.000Z"]
console.log(localUTCOccurrences) // [Tue Oct 29 2019 10:02:26 GMT-0700 (Pacific Daylight Time), Tue Nov 05 2019 09:02:26 GMT-0800 (Pacific Standard Time)]

const occurrences = localUTCOccurrences.map(setUTCPartsToDate)
console.log(occurrences.map(toISOString)) // ["2019-10-30T00:02:26.000Z", "2019-11-06T01:02:26.000Z"]
console.log(occurrences) // [Tue Oct 29 2019 17:02:26 GMT-0700 (Pacific Daylight Time), Tue Nov 05 2019 17:02:26 GMT-0800 (Pacific Standard Time)]

function setPartsToUTCDate(d) {
  return new Date(
    Date.UTC(
      d.getFullYear(),
      d.getMonth(),
      d.getDate(),
      d.getHours(),
      d.getMinutes(),
      d.getSeconds()
    )
  )
}

function setUTCPartsToDate(d) {
  return new Date(
    d.getUTCFullYear(),
    d.getUTCMonth(),
    d.getUTCDate(),
    d.getUTCHours(),
    d.getUTCMinutes(),
    d.getUTCSeconds()
  )
}

function toISOString(d) {
  return d.toISOString()
}

https://codesandbox.io/s/rrule-localutc-conversion-zxlki

或者简而言之:

import { RRule } from 'rrule'

const date = new Date('2019-10-29T00:02:26Z')

const rule = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.TU],
  count: 2,
  dtstart: setPartsToUTCDate(date)
})

const occurrences = rule.all().map(setUTCPartsToDate)

https://codesandbox.io/s/rrule-localutc-conversion-in-short-ez7g0

据我所知,Chrome、Firefox 和 Safari 在登录到控制台时都将日期显示为本地时间的字符串,而 Node.js 则显示为 UTC 中的 ISO 字符串。

所有18条评论

我也很迷茫。

好的,我明白了,自述文件中有这个关键段落:

image

您必须对本地时区使用这两个辅助功能。 如果要修改它以支持 tzid,则必须修改偏移量以获取 X 时区的偏移量。

/**
 * Mutates date. Add timezone to the date.
 *
 * <strong i="9">@param</strong> {Date} [date]
 */
export function localizeDate(date = new Date()) {
  const offset = new Date().getTimezoneOffset() * 60 * 1000; // get offset for local timezone (can modify here to support tzid)
  date.setTime(date.getTime() - offset);
  return date;
}

/**
 * Mutates a date. De-timezones a date.
 *
 * <strong i="10">@param</strong> {Date} [date]
 */
export function utcifyDate(date = new Date()) {
  const offset = new Date().getTimezoneOffset() * 60 * 1000; // get offset for local timezone (can modify here to support tzid)
  date.setTime(date.getTime() + offset);
  return date;
}

然后您的代码将更改为:

const { RRule, RRuleSet } = require(`rrule`);

const dtstart = localizeDate(new Date());
console.log(`dtstart`, utcifyDate(new Date(dtstart))); // even though it is utc string, this is the local time

const firstHour = dtstart.getUTCHours();
const secondHour = (firstHour + 2) % 24;

const getRRule = options => new RRule(Object.assign({}, options, {
    freq: RRule.DAILY,
    count: 1,
    dtstart,
    byminute: 0,
    bysecond: 0
}));

const showNextInstance = tzid => {
    const ruleSet = new RRuleSet();
    ruleSet.rrule(getRRule({ byhour: firstHour })); // remove tzid as i dont support this
    ruleSet.rrule(getRRule({ byhour: secondHour })); // removed tzid as i dont support this
    console.log('local:', utcifyDate(ruleSet.after(dtstart));
};

showNextInstance(undefined);
showNextInstance(`UTC`);
showNextInstance(`local`);
showNextInstance(`America/Chicago`);

我怀疑在这个库中不会有一个很好的方法来解决这个问题,因为它建立在 JS 令人困惑和不规则的Date实现上。 我目前正在完全重写,但需要一些时间才能做到正确。

谢谢@davidgoli,您的工作非常感谢!!!

我觉得我们需要懂的人写一篇文章来帮助别人理解不规则日期的实现。 老实说,我仍然不明白,我只是调整了一些东西直到它起作用。

这是因为您打印到控制台的返回日期是一个正在转换为字符串的 JavaScript 日期,并且 JavaScript 会自动以 UTC 格式将日期打印到控制台(请参阅时间戳末尾的“Z”,表示零偏移)。 由于您的四种情况中的每一种情况下的下一次发生都在完全相同的时间(尽管在不同的时区),因此它会打印四次完全相同的时间戳。

另请注意,JavaScript Date对象使用自 1970 年 1 月 1 日星期四 00:00:00 UTC(注意那里的“UTC”!)以来经过的毫秒数来跟踪时间,而不是某些“12:34:56pm”或任何类似的字符串解释。

@davidgoli

我目前正在完全重写,但需要一些时间才能做到正确。

您可能想查看rSchedule 的源代码。 所有迭代都是使用特殊的DateTime对象(不是来自 Luxon)完成的,该看起来与输入等效的浮动 UTC 日期。 迭代是用这个 UTC 日期时间完成的,所以 DST 不是问题。 发现事件后,在将其交给用户之前,它会被转换回适当时区的正常日期。

例如:这个日期

import { DateTime as LuxonDateTime } from 'luxon';

const date = LuxonDateTime.fromObject({
  year: 2019,
  month: 1,
  day: 1,
  zone: 'America/Los_Angeles'
})

将转换为此 UTC 日期并保存时区

const fakeDate = Date.UTC(2019, 0, 1)
const fakeDateZone = 'America/Los_Angeles'

请注意,此 UTC 日期看起来与 Luxon 日期相似(它们都看起来像2019/1/1 ),但是,由于它们位于不同的时区,因此它们实际上并不代表相同的时间。

话虽如此,使用这个特殊的“UTC”日期进行迭代可以让我们进行迭代而不必担心夏令时。 找到正确的日期后,我们可以将 is 转换回 luxon 日期。

例如:

LuxonDateTime.fromObject({
  year: fakeDate.year,
  month: fakeDate.month,
  day: fakeDate.day,
  zone: fakeDateZone,
})

这使迭代逻辑保持良好的 UTC 友好,同时仍支持任意时区。

@thefliik我把原语

啊呀。 byweekno很烦人。

任何新闻?

我认为很容易将Date对象的问题和RRULE计算出现的问题混为一谈,所以我将在将两者结合之前分别讨论它们。 我不得不把这个主要写出来,这样我才能理清我对这个问题的理解,并想我会在这里发帖,以防它对其他人有帮助。

JavaScript 日期对象

我不同意 JS Date对象是不规则的,但是与对象之间存在常见的误解,这确实使它看起来如此。 至少可以说是令人困惑的。

正如@hlee5zebra 所提到的,使用Date构造函数创建的日期始终以 UTC 表示。 它们可能以当地时间显示,但在幕后,它们实际上以 Unix 纪元时间表示。 我们总是可以在实例化的Date对象上运行.getTime().toISOString()来提醒自己我们在 UTC 下工作。

该实现还正确地遵循了以 UTC 进行所有处理并且仅转换为本地时间显示给用户的约定。 碰巧我们,开发者,是用户; 但是我们有自己的用户,这让水变得浑浊。 在开发我们的应用程序时,我们必须确保遵循约定并将日期保存为 UTC 在我们的后端(JS 已经以 UTC 处理日期,所以我们在那里很好)。 如果您将日期作为 UTC 存储在您的数据存储中,它们应该作为ISO 字符串到达客户端,由Date对象正确解析,并以用户的本地时间显示,所有这些都无需做任何繁重的工作。

这篇文章帮助我理解了Date对象并发现了我可以用它做什么: https :

规则

计算RRULE出现次数变得棘手,因为用户期望和 UTC 中的日期处理存在根本差异,当这两个时间表示跨越一天的障碍时,这一点很明显。

例如,如果我们的用户希望每个星期二发生一次,他们就会期望所有事件都发生在他们当地时间的星期二。 但是,所有计算都是在 UTC 中完成的,所以RRULE像:

DTSTART:2019-10-29T00:02:26Z
RRULE:FREQ=WEEKLY;BYWEEKDAY=TU

将在 UTC 返回星期二发生的事件,但对于位于本初子午线以西的任何用户,事件将出现在当地时间的星期一,这显然不是任何人希望的结果。

做对了

综上所述,开发人员必须做的就是强制 JavaScript 认为本地日期实际上是 UTC 日期,将“假”日期赋予rrule.js ,并让库在此本地 UTC 时间。 当您从.all()方法中获取事件时,您可以使用这些日期的“UTC”部分实例化新的Date对象。 这有效地使结果符合预期。

import { RRule } from 'rrule'

const dtstart = new Date('2019-10-29T00:02:26Z') // same as: `new Date(Date.UTC(2019, 9, 29, 0, 2, 26))`
console.log(dtstart.toISOString()) // 2019-10-29T00:02:26.000Z
console.log(dtstart) // Mon Oct 28 2019 17:02:26 GMT-0700 (Pacific Daylight Time)

const fakeDateStart = setPartsToUTCDate(dtstart)
console.log(fakeDateStart.toISOString()) // 2019-10-28T17:02:26.000Z
console.log(fakeDateStart) // Mon Oct 28 2019 10:02:26 GMT-0700 (Pacific Daylight Time)

const rule = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.TU],
  count: 2,
  dtstart: fakeDateStart
})

const localUTCOccurrences = rule.all()
console.log(localUTCOccurrences.map(toISOString)) // ["2019-10-29T17:02:26.000Z", "2019-11-05T17:02:26.000Z"]
console.log(localUTCOccurrences) // [Tue Oct 29 2019 10:02:26 GMT-0700 (Pacific Daylight Time), Tue Nov 05 2019 09:02:26 GMT-0800 (Pacific Standard Time)]

const occurrences = localUTCOccurrences.map(setUTCPartsToDate)
console.log(occurrences.map(toISOString)) // ["2019-10-30T00:02:26.000Z", "2019-11-06T01:02:26.000Z"]
console.log(occurrences) // [Tue Oct 29 2019 17:02:26 GMT-0700 (Pacific Daylight Time), Tue Nov 05 2019 17:02:26 GMT-0800 (Pacific Standard Time)]

function setPartsToUTCDate(d) {
  return new Date(
    Date.UTC(
      d.getFullYear(),
      d.getMonth(),
      d.getDate(),
      d.getHours(),
      d.getMinutes(),
      d.getSeconds()
    )
  )
}

function setUTCPartsToDate(d) {
  return new Date(
    d.getUTCFullYear(),
    d.getUTCMonth(),
    d.getUTCDate(),
    d.getUTCHours(),
    d.getUTCMinutes(),
    d.getUTCSeconds()
  )
}

function toISOString(d) {
  return d.toISOString()
}

https://codesandbox.io/s/rrule-localutc-conversion-zxlki

或者简而言之:

import { RRule } from 'rrule'

const date = new Date('2019-10-29T00:02:26Z')

const rule = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.TU],
  count: 2,
  dtstart: setPartsToUTCDate(date)
})

const occurrences = rule.all().map(setUTCPartsToDate)

https://codesandbox.io/s/rrule-localutc-conversion-in-short-ez7g0

据我所知,Chrome、Firefox 和 Safari 在登录到控制台时都将日期显示为本地时间的字符串,而 Node.js 则显示为 UTC 中的 ISO 字符串。

@tim-phillips 我现在也为此感到痛苦,正在寻找某种猴子补丁来解决它。

我在美国东部时间晚上 8 点有一个重复发生的事件,效果很好,但美国东部时间晚上 9 点将它推回到应该发生的前一天。

期待你能不能找到这个问题的答案。

@tim-Phillips 这是一个很好的细分。 我还在经历它,但看起来它会帮助我最终理解,谢谢!

@tim-Phillips 感谢您的分析。

但是,您能解释以下内容吗? 根据您计算机的本地时区,您的解决方案似乎仍然可以在星期三返回。

image

@tonylau不要查看控制台中的字符串化输出,这些输出将始终转换为您的本地时区。 您应该使用getUTC*()方法来获取日期的各个部分,无论您的本地时区如何,这些部分都是相同的。

@davidgoli

我将时区设置为 UTC+14(Kiritimati Island/Line Islands Time)并得到以下结果,即使我使用的是 getUTCDay()。 当它应该是星期二时,这一天仍然返回为星期三?

沙盒: https :

image

可能与本次讨论相关: https :

@tim-Phillips 谢谢!!!!!!!!! 这很有帮助!!!!!!! 你碰巧知道如何用规则制作 ics 吗? 当我在节点中使用 ics 包时,我又遇到了一天的问题,当我收到日历邀请时,他们休息了一天。 (但是由于您的代码,我能够正确地将所有内容放入我的数据库中!!非常感谢。)

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

jimmywarting picture jimmywarting  ·  9评论

espen picture espen  ·  11评论

kirrg001 picture kirrg001  ·  5评论

michaelkrog picture michaelkrog  ·  9评论

fatshotty picture fatshotty  ·  5评论