我目前正在使用在服务器端和客户端上使用 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
的破损情况?我想我想要这个:
将持续时间(增量)应用于独立于本地时区的日期的纯函数。理想情况下,它应该与 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>
我的理解是需要一个仅在 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,
};
})
);