I'm currently working with software that does computations with date-fns Durations both on the server- and client-side.
This software gathers data for a time window that is specified using Durations from a URL. The intention then is to gather data and perform computations on both sides for the same time window.
Now because of DST there are cases where these windows do not align when adding the Durations to a current date on either end.
For example when computing add(new Date('2023-11-13T10:59:13.371Z'), { days: -16 })
in UTC the computation arrives at 2023-10-28T10:59:13.371Z
, but a browser in CET will arrive at 2023-10-28T09:59:13.371Z
instead.
Attempted solution
I've been trying to conjure up a special addDuration
function to add durations the way UTC does it in the hope of obtaining a reproducible way to apply durations independent of Browsers. However (because time is hard) this appears quite hard to get right and I'm not sure it is even entirely possible with what we've got. (I wish temporal was ready to aid me in this.)
So I came up with this function:
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 })
}
I then went on to test it with several examples and print outputs like this:
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 }
}),
)
With this I went ahead and executed the code with different TZ
environment variables:
Output of TZ=UTC node example.js
:
Output of TZ=CET node example.js
:
Here in the add2
column we see how addDuration
behaves and a ✅ is displayed in the ok2
column when it matches the UTC output. Similarly add1
is the behaviour of the typical date-fns/add
function.
Open ends
I'd like to specifically learn more about these aspects:
- Is it generally possible to apply Durations to a Date in Browsers without shipping a whole dump of different timezone data?
- Is there a simple way to correct the broken case for
addDuration
inTZ=CET
?
- Is there a simple way to correct the broken case for
- Is there an easy/simple way to achieve the desired outcome using date-fns? Maybe I've just overlooked something?
- Is what I'm trying here a bad idea for some reason and I just struggle to understand that?
I think I want this:
A pure function to apply Durations (deltas) to a Date independent of the local timezone. Ideally it should work the same as UTC, but that feels secondary to working the same across different browsers.
I'm under the impression that this is hindered to some extent by how Date in JavaScript behaves dependent on the local TZ.
Existence of such a function would - I think - imply that a statement such as 'yesterday' or '1 year ago' could be interpreted in a way that makes sense independent of the local TZ and independent of DST.
I know that it would be possible to gloss over the facts of how many days the current year or month have exactly and 'just' compute a number of hours for this, to then accept the same delta for all - but I'd like things like { months: -1 }
to work in a way that makes 'sense' for humans if possible.
Related notes
- I've also opened a discussion about this on date-fns GitHub.
- I've had looks at both date-fns-tz and date-fns/utc and could not find good ways to use them.
Complete example
Here's the complete example.js
source:
// 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>
from Adding Durations to dates in a manner stable across timezones
No comments:
Post a Comment