提问人:Jakob Runge 提问时间:11/15/2023 最后编辑:Jakob Runge 更新时间:11/21/2023 访问量:93
以跨时区稳定的方式向日期添加持续时间
Adding Durations to dates in a manner stable across timezones
问:
我目前正在使用在服务器端和客户端使用 date-fns 持续时间进行计算的软件。
该软件收集使用来自 URL 的持续时间指定的时间窗口的数据。然后,目的是在同一时间窗口内收集数据并在双方执行计算。
现在,由于 DST,在某些情况下,在将持续时间添加到任一端的当前日期时,这些窗口不会对齐。
例如,当以 UTC 计算时,计算到达 ,但 CET 中的浏览器将到达 。add(new Date('2023-11-13T10:59:13.371Z'), { days: -16 })
2023-10-28T10:59:13.371Z
2023-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
在列中,我们看到行为方式,当它与 UTC 输出匹配时,列中会显示 a ✅。典型函数的行为也是如此。add2
addDuration
ok2
add1
date-fns/add
开口端
我想具体了解这些方面:
- 通常是否可以在浏览器中将持续时间应用于日期,而无需传送不同时区数据的整个转储?
- 有没有一种简单的方法可以纠正 in 的破损大小写?
addDuration
TZ=CET
- 有没有一种简单的方法可以纠正 in 的破损大小写?
- 有没有一种简单易行的方法可以使用 date-fns 实现预期的结果?也许我只是忽略了什么?
- 出于某种原因,我在这里尝试的东西是一个坏主意,我只是很难理解吗?
我想我想要这个:
一个纯函数,用于将持续时间(增量)应用于独立于本地时区的日期。理想情况下,它应该与 UTC 的工作方式相同,但这感觉是次要的,而不是在不同的浏览器上工作相同的。
我的印象是,这在某种程度上受到 JavaScript 中 Date 的行为方式依赖于本地 TZ 的阻碍。
我认为,这种函数的存在意味着,像“昨天”或“1 年前”这样的陈述可以以一种独立于本地 TZ 和独立于 DST 的有意义的方式进行解释。
我知道有可能掩盖当前年份或月份确切有多少天的事实,并“只是”为此计算小时数,然后接受所有人的相同增量 - 但我希望事情以一种对人类“有意义”的方式工作如果可能的话。{ months: -1 }
相关说明
- 我也在 date-fns GitHub 上就此展开了讨论。
- 我看过 date-fns-tz 和 date-fns/utc,但找不到使用它们的好方法。
完整示例
以下是完整的来源: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>
答:
我的理解是需要一个仅在 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,
};
})
);
评论