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

问题描述 投票:0回答:1

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

该软件收集使用 URL 中的持续时间指定的时间窗口的数据。其目的是在同一时间窗口内收集数据并在两侧执行计算。

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

例如,在 UTC 中计算

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

尝试过的解决方案

我一直在尝试想出一个特殊的

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

所以我想出了这个功能:

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.js
的输出:

TZ=CET node example.js
的输出:

add2
列中,我们可以看到
addDuration
的行为方式,并且当它与 UTC 输出匹配时,
ok2
列中会显示 ✅。同样,
add1
是典型
date-fns/add
函数的行为。

两端开放

我想具体了解一下这几个方面:

  • 通常是否可以在浏览器中将持续时间应用于日期而不传送不同时区数据的整个转储?
    • 有没有一种简单的方法来纠正
      addDuration
      TZ=CET
      的破损情况?
  • 有没有一种简单/简单的方法可以使用 date-fns 达到预期的结果?也许我只是忽略了一些事情?
  • 出于某种原因,我在这里尝试的想法是不是一个坏主意,而我只是很难理解这一点?

我想我想要这个:

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

我的印象是,这在某种程度上受到了 JavaScript 中日期依赖于本地 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 node.js dst date-fns date-fns-tz
1个回答
0
投票

我的理解是需要一个仅在 UTC 中计算的

addDuration
函数,我认为使用
Date.UTC
Date.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

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

完整代码如下所示:

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,
    };
  })
);
© www.soinside.com 2019 - 2024. All rights reserved.