以跨时区稳定的方式向日期添加持续时间

Adding Durations to dates in a manner stable across timezones

提问人:Jakob Runge 提问时间:11/15/2023 最后编辑:Jakob Runge 更新时间:11/21/2023 访问量:93

问:

赏金将在 3 天后到期。这个问题的答案有资格获得 +50 声望赏金。雅各布·朗格(Jakob Runge)正在寻找信誉良好的消息来源的答案
如果可能,请提供一个 addDuration 函数的示例,该函数在 UTC 中独立于本地 TZ 工作,或者至少在不同浏览器/系统之间以稳定的方式工作。
如果解决方案建立在 date-fns 或 vanilla JS 功能之上,那就太好了,但任何日期/时间库都优先于没有。
如果无法做到这一点,请解释原因,并链接来源(如果知道)。

我目前正在使用在服务器端和客户端使用 date-fns 持续时间进行计算的软件。

该软件收集使用来自 URL 的持续时间指定的时间窗口的数据。然后,目的是在同一时间窗口内收集数据并在双方执行计算。

现在,由于 DST,在某些情况下,在将持续时间添加到任一端的当前日期时,这些窗口不会对齐。

例如,当以 UTC 计算时,计算到达 ,但 CET 中的浏览器将到达 。add(new Date('2023-11-13T10:59:13.371Z'), { days: -16 })2023-10-28T10:59:13.371Z2023-10-28T09:59:13.371Z

尝试的解决方案

我一直在尝试变出一个特殊的函数来像UTC那样添加持续时间,希望获得一种可重复的方式来应用独立于浏览器的持续时间。然而(因为时间很艰难),这似乎很难做到正确,我不确定我们所拥有的是否完全可能。(我希望 temporal 准备好帮助我。addDuration

所以我想出了这个功能:

const addDuration = (date, delta) => {
  const { years = 0, months = 0, weeks = 0, days = 0, hours = 0, minutes = 0, seconds = 0 } = delta

  const dateWithCalendarDelta = add(date, { months, years, days, weeks })
  const tzDelta = date.getTimezoneOffset() - dateWithCalendarDelta.getTimezoneOffset()

  return add(dateWithCalendarDelta, { hours, minutes: minutes + tzDelta, seconds })
}

然后,我继续用几个示例和打印输出来测试它,如下所示:

console.table(
  examples.map(({ start, delta, utc }) => {
    const add1 = add(new Date(start), delta)
    const ok1 = add1.toISOString() === utc ? '✅' : '❌'
    const add2 = addDuration(new Date(start), delta)
    const ok2 = add2.toISOString() === utc ? '✅' : '❌'

    return { start: new Date(start), delta, utc: new Date(utc), add1, ok1, add2, ok2 }
  }),
)

有了这个,我继续使用不同的环境变量执行代码:TZ

输出:TZ=UTC node example.jsScreenshot of TZ=UTC node example.js

输出:TZ=CET node example.jsScreenshot of TZ=CET node example.js

在列中,我们看到行为方式,当它与 UTC 输出匹配时,列中会显示 a ✅。典型函数的行为也是如此。add2addDurationok2add1date-fns/add

开口端

我想具体了解这些方面:

  • 通常是否可以在浏览器中将持续时间应用于日期,而无需传送不同时区数据的整个转储?
    • 有没有一种简单的方法可以纠正 in 的破损大小写?addDurationTZ=CET
  • 有没有一种简单易行的方法可以使用 date-fns 实现预期的结果?也许我只是忽略了什么?
  • 出于某种原因,我在这里尝试的东西是一个坏主意,我只是很难理解吗?

我想我想要这个:

一个纯函数,用于将持续时间(增量)应用于独立于本地时区的日期。理想情况下,它应该与 UTC 的工作方式相同,但这感觉是次要的,而不是在不同的浏览器上工作相同的。

我的印象是,这在某种程度上受到 JavaScript 中 Date 的行为方式依赖于本地 TZ 的阻碍。

我认为,这种函数的存在意味着,像“昨天”或“1 年前”这样的陈述可以以一种独立于本地 TZ 和独立于 DST 的有意义的方式进行解释。

我知道有可能掩盖当前年份或月份确切有多少天的事实,并“只是”为此计算小时数,然后接受所有人的相同增量 - 但我希望事情以一种对人类“有意义”的方式工作如果可能的话。{ months: -1 }

相关说明

完整示例

以下是完整的来源:example.js

// const add = require('date-fns/add')

const examples = [{
    start: '2023-10-29T03:00:00.000Z',
    delta: {
      hours: 0
    },
    utc: '2023-10-29T03:00:00.000Z',
  },
  {
    start: '2023-10-29T03:00:00.000Z',
    delta: {
      hours: -1
    },
    utc: '2023-10-29T02:00:00.000Z',
  },
  {
    start: '2023-10-29T03:00:00.000Z',
    delta: {
      hours: -2
    },
    utc: '2023-10-29T01:00:00.000Z',
  },
  {
    start: '2023-10-29T03:00:00.000Z',
    delta: {
      hours: -3
    },
    utc: '2023-10-29T00:00:00.000Z',
  },
  {
    start: '2023-10-29T03:00:00.000Z',
    delta: {
      hours: -4
    },
    utc: '2023-10-28T23:00:00.000Z',
  },
  {
    start: '2023-11-13T10:59:13.371Z',
    delta: {
      days: -15,
      hours: -4
    },
    utc: '2023-10-29T06:59:13.371Z',
  },
  {
    start: '2023-11-13T10:59:13.371Z',
    delta: {
      days: -16
    },
    utc: '2023-10-28T10:59:13.371Z',
  },
  {
    start: '2023-11-13T10:59:13.371Z',
    delta: {
      days: -16,
      hours: -4
    },
    utc: '2023-10-28T06:59:13.371Z',
  },
  {
    start: '2023-11-13T10:59:13.371Z',
    delta: {
      hours: -(16 * 24 + 4)
    },
    utc: '2023-10-28T06:59:13.371Z',
  },
  {
    start: '2023-10-30T00:00:00.000Z',
    delta: {
      days: -1
    },
    utc: '2023-10-29T00:00:00.000Z',
  },
  {
    start: '2023-10-30T00:00:00.000Z',
    delta: {
      days: -2
    },
    utc: '2023-10-28T00:00:00.000Z',
  },
  {
    start: '2023-03-26T04:00:00.000Z',
    delta: {
      hours: 0
    },
    utc: '2023-03-26T04:00:00.000Z',
  },
  {
    start: '2023-03-26T04:00:00.000Z',
    delta: {
      hours: -1
    },
    utc: '2023-03-26T03:00:00.000Z',
  },
  {
    start: '2023-03-26T04:00:00.000Z',
    delta: {
      hours: -2
    },
    utc: '2023-03-26T02:00:00.000Z',
  },
  {
    start: '2023-03-26T04:00:00.000Z',
    delta: {
      hours: -3
    },
    utc: '2023-03-26T01:00:00.000Z',
  },
  {
    start: '2023-03-26T04:00:00.000Z',
    delta: {
      days: -1
    },
    utc: '2023-03-25T04:00:00.000Z',
  },
  {
    start: '2023-03-26T04:00:00.000Z',
    delta: {
      days: -1,
      hours: 1
    },
    utc: '2023-03-25T05:00:00.000Z',
  },
  {
    start: '2023-10-30T00:00:00.000Z',
    delta: {
      months: 1,
      days: -1
    },
    utc: '2023-11-29T00:00:00.000Z',
  },
  {
    start: '2023-10-30T00:00:00.000Z',
    delta: {
      months: -1,
      days: 1
    },
    utc: '2023-10-01T00:00:00.000Z',
  },
  {
    start: '2023-10-30T00:00:00.000Z',
    delta: {
      years: 1,
      days: -1
    },
    utc: '2024-10-29T00:00:00.000Z',
  },
  {
    start: '2023-10-30T00:00:00.000Z',
    delta: {
      years: -1,
      days: 1
    },
    utc: '2022-10-31T00:00:00.000Z',
  },
  {
    start: '2023-10-29T00:00:00.000Z',
    delta: {
      months: 1,
      days: -1
    },
    utc: '2023-11-28T00:00:00.000Z',
  },
  {
    start: '2023-10-29T00:00:00.000Z',
    delta: {
      months: -1,
      days: 1
    },
    utc: '2023-09-30T00:00:00.000Z',
  },
  {
    start: '2023-10-29T00:00:00.000Z',
    delta: {
      years: 1,
      days: -1
    },
    utc: '2024-10-28T00:00:00.000Z',
  },
  {
    start: '2023-10-29T00:00:00.000Z',
    delta: {
      years: -1,
      days: 1
    },
    utc: '2022-10-30T00:00:00.000Z',
  },
  {
    start: '2023-10-28T00:00:00.000Z',
    delta: {
      months: 1,
      days: -1
    },
    utc: '2023-11-27T00:00:00.000Z',
  },
  {
    start: '2023-10-28T00:00:00.000Z',
    delta: {
      months: -1,
      days: 1
    },
    utc: '2023-09-29T00:00:00.000Z',
  },
  {
    start: '2023-10-28T00:00:00.000Z',
    delta: {
      years: 1,
      days: -1
    },
    utc: '2024-10-27T00:00:00.000Z',
  },
  {
    start: '2023-10-28T00:00:00.000Z',
    delta: {
      years: -1,
      days: 1
    },
    utc: '2022-10-29T00:00:00.000Z',
  },
  {
    start: '2023-03-27T00:00:00.000Z',
    delta: {
      months: 1,
      days: -1
    },
    utc: '2023-04-26T00:00:00.000Z',
  },
  {
    start: '2023-03-27T00:00:00.000Z',
    delta: {
      months: -1,
      days: 1
    },
    utc: '2023-02-28T00:00:00.000Z',
  },
  {
    start: '2023-03-27T00:00:00.000Z',
    delta: {
      years: 1,
      days: -1
    },
    utc: '2024-03-26T00:00:00.000Z',
  },
  {
    start: '2023-03-27T00:00:00.000Z',
    delta: {
      years: -1,
      days: 1
    },
    utc: '2022-03-28T00:00:00.000Z',
  },
  {
    start: '2023-03-26T00:00:00.000Z',
    delta: {
      months: 1,
      days: -1
    },
    utc: '2023-04-25T00:00:00.000Z',
  },
  {
    start: '2023-03-26T00:00:00.000Z',
    delta: {
      months: -1,
      days: 1
    },
    utc: '2023-02-27T00:00:00.000Z',
  },
  {
    start: '2023-03-26T00:00:00.000Z',
    delta: {
      years: 1,
      days: -1
    },
    utc: '2024-03-25T00:00:00.000Z',
  },
  {
    start: '2023-03-26T00:00:00.000Z',
    delta: {
      years: -1,
      days: 1
    },
    utc: '2022-03-27T00:00:00.000Z',
  },
  {
    start: '2023-03-25T00:00:00.000Z',
    delta: {
      months: 1,
      days: -1
    },
    utc: '2023-04-24T00:00:00.000Z',
  },
  {
    start: '2023-03-25T00:00:00.000Z',
    delta: {
      months: -1,
      days: 1
    },
    utc: '2023-02-26T00:00:00.000Z',
  },
  {
    start: '2023-03-25T00:00:00.000Z',
    delta: {
      years: 1,
      days: -1
    },
    utc: '2024-03-24T00:00:00.000Z',
  },
  {
    start: '2023-03-25T00:00:00.000Z',
    delta: {
      years: -1,
      days: 1
    },
    utc: '2022-03-26T00:00:00.000Z',
  },
]

const addDuration = (date, delta) => {
  const {
    years = 0, months = 0, weeks = 0, days = 0, hours = 0, minutes = 0, seconds = 0
  } = delta

  const dateWithCalendarDelta = add(date, {
    months,
    years,
    days,
    weeks
  })
  const tzDelta = date.getTimezoneOffset() - dateWithCalendarDelta.getTimezoneOffset()

  return add(dateWithCalendarDelta, {
    hours,
    minutes: minutes + tzDelta,
    seconds
  })
}

const main = () => {
  console.table(
    examples.map(({
      start,
      delta,
      utc
    }) => {
      const add1 = add(new Date(start), delta)
      const ok1 = add1.toISOString() === utc ? '✅' : '❌'
      const add2 = addDuration(new Date(start), delta)
      const ok2 = add2.toISOString() === utc ? '✅' : '❌'

      return {
        start: new Date(start),
        delta,
        utc: new Date(utc),
        add1,
        ok1,
        add2,
        ok2
      }
      document.querySelector('tbody')
    }),
  )
}

setTimeout(main, 500)
<script type="module">
  import { add } from 'https://esm.run/date-fns';
  window.add = add;
</script>

JavaScript 节点:.js dst date-fns-tz

评论

1赞 mplungjan 11/15/2023
请使代码段可运行
1赞 mplungjan 11/15/2023
您需要 cdn.jsdelivr.net/npm/[email protected]/index.min.js而不是导入它
0赞 Jakob Runge 11/16/2023
嗯 - 我很难找出如何正确地将其包含在代码片段中并使用它。我也用 unpkg.com/[email protected]/index 试过运气.js但在这两种情况下,我都会收到脚本抱怨要求不在场(同意)。不过,我也想让代码片段可运行 - 感谢您的指针:)
0赞 mplungjan 11/16/2023
这不是微不足道的,但我做到了。我们需要一个 type=“module”
1赞 Jakob Runge 11/16/2023
是的 - 对不起,我正在回滚编辑,因为我发现工作片段使探索问题本身变得更加困难。对我来说,拥有有效的控制台输出以及复制和播放代码的能力似乎是更好的交易。

答:

0赞 Jakob Runge 11/21/2023 #1

我的理解是需要一个仅在 UTC 中计算的函数,我认为可以使用这样的函数:addDurationDate.UTCDate.getUTC*

const addDuration = (date, delta) => {
  const {
    years = 0,
    months = 0,
    weeks = 0,
    days = 0,
    hours = 0,
    minutes = 0,
    seconds = 0,
  } = delta;

  const utcYears = date.getUTCFullYear();
  const utcMonths = date.getUTCMonth();
  const utcDays = date.getUTCDate();
  const utcHours = date.getUTCHours();
  const utcMinutes = date.getUTCMinutes();
  const utcSeconds = date.getUTCSeconds();
  const utcMilliseconds = date.getUTCMilliseconds();

  return new Date(
    Date.UTC(
      utcYears + years,
      utcMonths + months,
      utcDays + weeks * 7 + days,
      utcHours + hours,
      utcMinutes + minutes,
      utcSeconds + seconds,
      utcMilliseconds
    )
  );
};

我认为它可以满足为独立于 TZ 的不同客户端计算相同增量的相同日期和开始日期的需要。

我知道在进行这些计算时,通常可能会有不同的结果。例如,当询问 3 月 31 日前一个月是哪个日期时。对于这些情况,对我来说,重要的是所有客户端的行为都是一样的,并且优先于 JS“无论如何都这样做”。我的理解是,当要求 JS 为这种情况创建日期时,就会发生这种情况:

new Date(Date.UTC(2023,01,31,0,0,0,0))
// 2023-03-03T00:00:00.000Z

我发现该实现也适合示例中的所有测试用例:

Screenshot of executing the given example with the new suggestion of addDuration in TZ=CET

完整的代码如下所示:

const add = require("date-fns/add");

const examples = [
  {
    start: "2023-10-29T03:00:00.000Z",
    delta: { hours: 0 },
    utc: "2023-10-29T03:00:00.000Z",
  },
  {
    start: "2023-10-29T03:00:00.000Z",
    delta: { hours: -1 },
    utc: "2023-10-29T02:00:00.000Z",
  },
  {
    start: "2023-10-29T03:00:00.000Z",
    delta: { hours: -2 },
    utc: "2023-10-29T01:00:00.000Z",
  },
  {
    start: "2023-10-29T03:00:00.000Z",
    delta: { hours: -3 },
    utc: "2023-10-29T00:00:00.000Z",
  },
  {
    start: "2023-10-29T03:00:00.000Z",
    delta: { hours: -4 },
    utc: "2023-10-28T23:00:00.000Z",
  },
  {
    start: "2023-11-13T10:59:13.371Z",
    delta: { days: -15, hours: -4 },
    utc: "2023-10-29T06:59:13.371Z",
  },
  {
    start: "2023-11-13T10:59:13.371Z",
    delta: { days: -16 },
    utc: "2023-10-28T10:59:13.371Z",
  },
  {
    start: "2023-11-13T10:59:13.371Z",
    delta: { days: -16, hours: -4 },
    utc: "2023-10-28T06:59:13.371Z",
  },
  {
    start: "2023-11-13T10:59:13.371Z",
    delta: { hours: -(16 * 24 + 4) },
    utc: "2023-10-28T06:59:13.371Z",
  },
  {
    start: "2023-10-30T00:00:00.000Z",
    delta: { days: -1 },
    utc: "2023-10-29T00:00:00.000Z",
  },
  {
    start: "2023-10-30T00:00:00.000Z",
    delta: { days: -2 },
    utc: "2023-10-28T00:00:00.000Z",
  },
  {
    start: "2023-03-26T04:00:00.000Z",
    delta: { hours: 0 },
    utc: "2023-03-26T04:00:00.000Z",
  },
  {
    start: "2023-03-26T04:00:00.000Z",
    delta: { hours: -1 },
    utc: "2023-03-26T03:00:00.000Z",
  },
  {
    start: "2023-03-26T04:00:00.000Z",
    delta: { hours: -2 },
    utc: "2023-03-26T02:00:00.000Z",
  },
  {
    start: "2023-03-26T04:00:00.000Z",
    delta: { hours: -3 },
    utc: "2023-03-26T01:00:00.000Z",
  },
  {
    start: "2023-03-26T04:00:00.000Z",
    delta: { days: -1 },
    utc: "2023-03-25T04:00:00.000Z",
  },
  {
    start: "2023-03-26T04:00:00.000Z",
    delta: { days: -1, hours: 1 },
    utc: "2023-03-25T05:00:00.000Z",
  },
  {
    start: "2023-10-30T00:00:00.000Z",
    delta: { months: 1, days: -1 },
    utc: "2023-11-29T00:00:00.000Z",
  },
  {
    start: "2023-10-30T00:00:00.000Z",
    delta: { months: -1, days: 1 },
    utc: "2023-10-01T00:00:00.000Z",
  },
  {
    start: "2023-10-30T00:00:00.000Z",
    delta: { years: 1, days: -1 },
    utc: "2024-10-29T00:00:00.000Z",
  },
  {
    start: "2023-10-30T00:00:00.000Z",
    delta: { years: -1, days: 1 },
    utc: "2022-10-31T00:00:00.000Z",
  },
  {
    start: "2023-10-29T00:00:00.000Z",
    delta: { months: 1, days: -1 },
    utc: "2023-11-28T00:00:00.000Z",
  },
  {
    start: "2023-10-29T00:00:00.000Z",
    delta: { months: -1, days: 1 },
    utc: "2023-09-30T00:00:00.000Z",
  },
  {
    start: "2023-10-29T00:00:00.000Z",
    delta: { years: 1, days: -1 },
    utc: "2024-10-28T00:00:00.000Z",
  },
  {
    start: "2023-10-29T00:00:00.000Z",
    delta: { years: -1, days: 1 },
    utc: "2022-10-30T00:00:00.000Z",
  },
  {
    start: "2023-10-28T00:00:00.000Z",
    delta: { months: 1, days: -1 },
    utc: "2023-11-27T00:00:00.000Z",
  },
  {
    start: "2023-10-28T00:00:00.000Z",
    delta: { months: -1, days: 1 },
    utc: "2023-09-29T00:00:00.000Z",
  },
  {
    start: "2023-10-28T00:00:00.000Z",
    delta: { years: 1, days: -1 },
    utc: "2024-10-27T00:00:00.000Z",
  },
  {
    start: "2023-10-28T00:00:00.000Z",
    delta: { years: -1, days: 1 },
    utc: "2022-10-29T00:00:00.000Z",
  },
  {
    start: "2023-03-27T00:00:00.000Z",
    delta: { months: 1, days: -1 },
    utc: "2023-04-26T00:00:00.000Z",
  },
  {
    start: "2023-03-27T00:00:00.000Z",
    delta: { months: -1, days: 1 },
    utc: "2023-02-28T00:00:00.000Z",
  },
  {
    start: "2023-03-27T00:00:00.000Z",
    delta: { years: 1, days: -1 },
    utc: "2024-03-26T00:00:00.000Z",
  },
  {
    start: "2023-03-27T00:00:00.000Z",
    delta: { years: -1, days: 1 },
    utc: "2022-03-28T00:00:00.000Z",
  },
  {
    start: "2023-03-26T00:00:00.000Z",
    delta: { months: 1, days: -1 },
    utc: "2023-04-25T00:00:00.000Z",
  },
  {
    start: "2023-03-26T00:00:00.000Z",
    delta: { months: -1, days: 1 },
    utc: "2023-02-27T00:00:00.000Z",
  },
  {
    start: "2023-03-26T00:00:00.000Z",
    delta: { years: 1, days: -1 },
    utc: "2024-03-25T00:00:00.000Z",
  },
  {
    start: "2023-03-26T00:00:00.000Z",
    delta: { years: -1, days: 1 },
    utc: "2022-03-27T00:00:00.000Z",
  },
  {
    start: "2023-03-25T00:00:00.000Z",
    delta: { months: 1, days: -1 },
    utc: "2023-04-24T00:00:00.000Z",
  },
  {
    start: "2023-03-25T00:00:00.000Z",
    delta: { months: -1, days: 1 },
    utc: "2023-02-26T00:00:00.000Z",
  },
  {
    start: "2023-03-25T00:00:00.000Z",
    delta: { years: 1, days: -1 },
    utc: "2024-03-24T00:00:00.000Z",
  },
  {
    start: "2023-03-25T00:00:00.000Z",
    delta: { years: -1, days: 1 },
    utc: "2022-03-26T00:00:00.000Z",
  },
];

/**
 *
 * @param {Date} date
 * @param {*} delta
 * @returns Date
 */
const addDuration = (date, delta) => {
  const {
    years = 0,
    months = 0,
    weeks = 0,
    days = 0,
    hours = 0,
    minutes = 0,
    seconds = 0,
  } = delta;

  const utcYears = date.getUTCFullYear();
  const utcMonths = date.getUTCMonth();
  const utcDays = date.getUTCDate();
  const utcHours = date.getUTCHours();
  const utcMinutes = date.getUTCMinutes();
  const utcSeconds = date.getUTCSeconds();
  const utcMilliseconds = date.getUTCMilliseconds();

  return new Date(
    Date.UTC(
      utcYears + years,
      utcMonths + months,
      utcDays + weeks * 7 + days,
      utcHours + hours,
      utcMinutes + minutes,
      utcSeconds + seconds,
      utcMilliseconds
    )
  );
};

console.table(
  examples.map(({ start, delta, utc }) => {
    const add1 = add(new Date(start), delta);
    const ok1 = add1.toISOString() === utc ? "✅" : "❌";
    const add2 = addDuration(new Date(start), delta);
    const ok2 = add2.toISOString() === utc ? "✅" : "❌";

    return {
      start: new Date(start),
      delta,
      utc: new Date(utc),
      add1,
      ok1,
      add2,
      ok2,
    };
  })
);