Last active
February 6, 2023 17:59
-
-
Save dr-skot/df3af7a3118bbaea9efa07a97c8b7ae2 to your computer and use it in GitHub Desktop.
addInTimeZone: timezone-aware version of the date-fns `add` function
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import addInTimeZone from './addInTimeZone'; | |
import { formatInTimeZone } from 'date-fns-tz'; | |
it('adds days + hours to a date, with timezone awareness', () => { | |
// add a day plus 3 hours to this time, and you'll cross a DST boundary | |
// in both Halifax and LA | |
const start = '2022-11-05T06:12:34.567Z'; | |
const startTime = new Date(start).getTime(); | |
// but in Halifax you cross it adding the day; in LA you cross it adding hours | |
// i.e. in Halifax you add a 25-hr day plus 3 hrs = 28 hrs, and clock time is +3 hrs | |
// in LA you add a 24-hr day plus 3 hrs = 27 hrs, and clock time is +2 hrs | |
// Halifax: 11/05 3:12am | |
// + 1 day = 11/06 3:12am (25 hrs because clock fell back at 2am) | |
// + 3 hrs = 11/06 6:12am | |
// total 28 hrs | |
expect(formatInTimeZone(start, 'America/Halifax', fmt)).toBe('2022-11-05 03:12:34.567'); | |
const laterInHalifax = addInTimeZone(start, 'America/Halifax', { days: 1, hours: 3 }); | |
// + 28 hrs | |
expect(laterInHalifax.getTime() - startTime).toBe(28 * 60 * 60 * 1000); | |
// clock time is +3 hrs | |
expect(formatInTimeZone(laterInHalifax, 'America/Halifax', fmt)).toBe( | |
'2022-11-06 06:12:34.567' | |
); | |
// LA: 11/04 11:12pm | |
// + 1 day = 11/05 11:12pm (24 hrs because clock didn't fall back yet) | |
// + 3 hrs = 11/06 1:12am (fall back at 2am) | |
// total 27 hrs | |
expect(formatInTimeZone(start, 'America/Los_Angeles', fmt)).toBe('2022-11-04 23:12:34.567'); | |
const laterInLA = addInTimeZone(start, 'America/Los_Angeles', { days: 1, hours: 3 }); | |
// + 27 hrs | |
expect(laterInLA.getTime() - startTime).toBe(27 * 60 * 60 * 1000); | |
// clock time is +2 hrs | |
expect(formatInTimeZone(laterInLA, 'America/Los_Angeles', fmt)).toBe('2022-11-06 01:12:34.567'); | |
}); | |
it('adds months with timezone awareness', () => { | |
// add a month to this time in Halifax and you cross a DST boundary; in LA you don't | |
const start = '2022-10-06T06:12:34.567Z'; | |
const startTime = new Date(start).getTime(); | |
// Halifax | |
expect(formatInTimeZone(start, 'America/Halifax', fmt)).toBe('2022-10-06 03:12:34.567'); | |
const oneDayLaterInHalifax = addInTimeZone(start, 'America/Halifax', { months: 1 }); | |
// +31 days + 1 hr in Halifax, because clock fell back at 2am | |
expect(oneDayLaterInHalifax.getTime() - startTime).toBe((31 * 24 + 1) * 60 * 60 * 1000); | |
expect(formatInTimeZone(oneDayLaterInHalifax, 'America/Halifax', fmt)).toBe( | |
'2022-11-06 03:12:34.567' | |
); | |
// LA | |
expect(formatInTimeZone(start, 'America/Los_Angeles', fmt)).toBe('2022-10-05 23:12:34.567'); | |
const oneDayLaterInLA = addInTimeZone(start, 'America/Los_Angeles', { months: 1 }); | |
// +31 days + 0 hrs in LA, because clock didn't fall back yet | |
expect(oneDayLaterInLA.getTime() - startTime).toBe(31 * 24 * 60 * 60 * 1000); | |
expect(formatInTimeZone(oneDayLaterInLA, 'America/Los_Angeles', fmt)).toBe( | |
'2022-11-05 23:12:34.567' | |
); | |
// to sum up, | |
expect(oneDayLaterInHalifax.getTime()).toBe(oneDayLaterInLA.getTime() + 60 * 60 * 1000); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as dateFns from 'date-fns'; | |
import dateFnsTz from 'date-fns-tz'; | |
export function addInTimeZone( | |
dirtyDate: Date | string | number, | |
timeZone: string, | |
duration: dateFns.Duration | |
) { | |
const { years, months, weeks, days, hours, minutes, seconds } = duration; | |
// separate date and time portions | |
const start = dateFnsTz.toDate(dirtyDate, { timeZone }); | |
const ymd = dateFnsTz.formatInTimeZone(start, timeZone, 'yyyy-MM-dd'); | |
const hms = dateFnsTz.formatInTimeZone(start, timeZone, 'HH:mm:ss.SSS'); | |
// add days and larger units to the date portion to get the target day | |
const targetDay = dateFns.add(new Date(ymd), { years, months, weeks, days }); | |
const newYmd = targetDay.toISOString().slice(0, 10); | |
// combine the new date portion with the original time portion | |
const targetDayWithStartTime = dateFnsTz.toDate(newYmd + ' ' + hms, { timeZone }); | |
// now add hours and smaller units | |
return dateFns.add(targetDayWithStartTime, { hours, minutes, seconds }); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment