From 4310200cf0490a2be9090fdd0711e9d342ae9caf Mon Sep 17 00:00:00 2001 From: Scott Shepherd Date: Sun, 5 Feb 2023 16:36:57 -0500 Subject: [PATCH] provide addInTimeZone function timezone-aware version of date-fns.add --- package.json | 10 ++++ src/addInTimeZone/index.js | 64 +++++++++++++++++++++++ src/addInTimeZone/test.js | 96 +++++++++++++++++++++++++++++++++++ src/fp/addInTimeZone/index.js | 8 +++ 4 files changed, 178 insertions(+) create mode 100644 src/addInTimeZone/index.js create mode 100644 src/addInTimeZone/test.js create mode 100644 src/fp/addInTimeZone/index.js diff --git a/package.json b/package.json index 034e083..96ad50a 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,11 @@ "import": "./esm/zonedTimeToUtc/index.js", "require": "./zonedTimeToUtc/index.js" }, + "./addInTimeZone": { + "types": "./addInTimeZone/index.d.ts", + "import": "./esm/addInTimeZone/index.js", + "require": "./addInTimeZone/index.js" + }, "./fp": { "types": "./fp/index.d.ts", "import": "./esm/fp/index.js", @@ -94,6 +99,11 @@ "types": "./fp/zonedTimeToUtc/index.d.ts", "import": "./esm/fp/zonedTimeToUtc/index.js", "require": "./fp/zonedTimeToUtc/index.js" + }, + "./fp/addInTimeZone": { + "types": "./fp/addInTimeZone/index.d.ts", + "import": "./esm/fp/addInTimeZone/index.js", + "require": "./fp/addInTimeZone/index.js" } }, "scripts": { diff --git a/src/addInTimeZone/index.js b/src/addInTimeZone/index.js new file mode 100644 index 0000000..15fb6d1 --- /dev/null +++ b/src/addInTimeZone/index.js @@ -0,0 +1,64 @@ +import dateFnsAdd from 'date-fns/add/index.js' +import toDate from '../toDate/index.js' +import formatInTimeZone from '../formatInTimeZone/index.js' + +/** + * @name addInTimeZone + * @category Common Helpers + * @summary Add the specified years, months, weeks, days, hours, minutes and seconds to the given date, in the given time zone. + * + * @description + * Add the specified years, months, weeks, days, hours, minutes and seconds to the given date, in the given time zone. + * The time zone can make a difference because of Daylight Savings Time. + * At 11pm Nov 4 2022 in LA, +1 day (= 11pm Nov 5) means +24 hrs. + * But at the same time in Halifax (3am Nov 5) +1 day (= 3am Nov 6) means +25 hrs, + * because the clocks fall back 1 hr at 2am. + * + * @param date - the date to be changed + * @param timeZone - the time zone to do the calculation in; can be an offset or IANA time zone + * @param duration - the object with years, months, weeks, days, hours, minutes and seconds to be added. Positive decimals will be rounded using `Math.floor`, decimals less than zero will be rounded using `Math.ceil`. + * + * | Key | Description | + * |----------------|------------------------------------| + * | years | Amount of years to be added | + * | months | Amount of months to be added | + * | weeks | Amount of weeks to be added | + * | days | Amount of days to be added | + * | hours | Amount of hours to be added | + * | minutes | Amount of minutes to be added | + * | seconds | Amount of seconds to be added | + * + * All values default to 0 + * + * @returns the new date with the duration added + * + * @example + * // 11pm Nov 4 in LA === 3am Nov 5 in Halifax (UTC 2022-11-05T06:00Z). + * // In LA, +1 day === 11pm Nov 5 === +24 hours, + * // but in Halifax +1 day === 3am Nov 6 === +25 hours, + * // because the clocks fall back 1 hour (in both places) at 2am Nov 6. + * const HOUR = 60 * 60 * 1000; + * const start = new Date('2022-11-05T06:00Z'); + * const resultLA = addInTimeZone(start, 'America/Los_Angeles', { days: 1 }); + * const resultHalifax = addInTimeZone(start, 'America/Halifax', { days: 1 }); + * console.log((resultLA.getTime() - start.getTime()) / HOUR); // => 24 + * console.log((resultHalifax.getTime() - start.getTime()) / HOUR); // => 25 + */ +export default function addInTimeZone(dirtyDate, timeZone, duration) { + const { years, months, weeks, days, hours, minutes, seconds } = duration + + // separate date and time portions + const start = toDate(dirtyDate, { timeZone }) + const ymd = formatInTimeZone(start, timeZone, 'yyyy-MM-dd') + const hms = formatInTimeZone(start, timeZone, 'HH:mm:ss.SSS') + + // add days and larger units to the date portion to get the target day + const targetDay = dateFnsAdd(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 = toDate(newYmd + ' ' + hms, { timeZone }) + + // now add hours and smaller units + return dateFnsAdd(targetDayWithStartTime, { hours, minutes, seconds }) +} diff --git a/src/addInTimeZone/test.js b/src/addInTimeZone/test.js new file mode 100644 index 0000000..c4bf28b --- /dev/null +++ b/src/addInTimeZone/test.js @@ -0,0 +1,96 @@ +// @flow +/* eslint-env mocha */ + +import assert from 'power-assert' +import formatInTimeZone from '../formatInTimeZone' +import addInTimeZone from '.' + +describe('addInTimeZone', () => { + const fmt = 'yyyy-MM-dd HH:mm:ss.SSS' + + it('adds days to a date, with timezone awareness', () => { + // add a day to this time in Halifax and you cross a DST boundary; in LA you don't + const start = '2022-11-05T06:12:34.567Z' + const startTime = new Date(start).getTime() + + // Halifax + assert(formatInTimeZone(start, 'America/Halifax', fmt) === '2022-11-05 03:12:34.567') + const oneDayLaterInHalifax = addInTimeZone(start, 'America/Halifax', { days: 1 }) + // +25 hrs in Halifax because clock fell back at 2am + assert(oneDayLaterInHalifax.getTime() - startTime === 25 * 60 * 60 * 1000) + assert( + formatInTimeZone(oneDayLaterInHalifax, 'America/Halifax', fmt) === '2022-11-06 03:12:34.567' + ) + + // LA + assert(formatInTimeZone(start, 'America/Los_Angeles', fmt) === '2022-11-04 23:12:34.567') + const oneDayLaterInLA = addInTimeZone(start, 'America/Los_Angeles', { days: 1 }) + // +24 hrs in LA because clock didn't fall back yet + assert(oneDayLaterInLA.getTime() - startTime === 24 * 60 * 60 * 1000) + assert( + formatInTimeZone(oneDayLaterInLA, 'America/Los_Angeles', fmt) === '2022-11-05 23:12:34.567' + ) + + // to sum up, + assert(oneDayLaterInHalifax.getTime() === oneDayLaterInLA.getTime() + 60 * 60 * 1000) + }) + + 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 + assert(formatInTimeZone(start, 'America/Halifax', fmt) === '2022-11-05 03:12:34.567') + const laterInHalifax = addInTimeZone(start, 'America/Halifax', { days: 1, hours: 3 }) + // + 28 hrs + assert(laterInHalifax.getTime() - startTime === 28 * 60 * 60 * 1000) + // clock time is +3 hrs + assert(formatInTimeZone(laterInHalifax, 'America/Halifax', fmt) === '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 + assert(formatInTimeZone(start, 'America/Los_Angeles', fmt) === '2022-11-04 23:12:34.567') + const laterInLA = addInTimeZone(start, 'America/Los_Angeles', { days: 1, hours: 3 }) + // + 27 hrs + assert(laterInLA.getTime() - startTime === 27 * 60 * 60 * 1000) + // clock time is +2 hrs + assert(formatInTimeZone(laterInLA, 'America/Los_Angeles', fmt) === '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 + assert(formatInTimeZone(start, 'America/Halifax', fmt) === '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 + assert(oneDayLaterInHalifax.getTime() - startTime === (31 * 24 + 1) * 60 * 60 * 1000) + assert( + formatInTimeZone(oneDayLaterInHalifax, 'America/Halifax', fmt) === '2022-11-06 03:12:34.567' + ) + + // LA + assert(formatInTimeZone(start, 'America/Los_Angeles', fmt) === '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 + assert(oneDayLaterInLA.getTime() - startTime === 31 * 24 * 60 * 60 * 1000) + assert( + formatInTimeZone(oneDayLaterInLA, 'America/Los_Angeles', fmt) === '2022-11-05 23:12:34.567' + ) + + // to sum up, + assert(oneDayLaterInHalifax.getTime() === oneDayLaterInLA.getTime() + 60 * 60 * 1000) + }) +}) diff --git a/src/fp/addInTimeZone/index.js b/src/fp/addInTimeZone/index.js new file mode 100644 index 0000000..bb338ee --- /dev/null +++ b/src/fp/addInTimeZone/index.js @@ -0,0 +1,8 @@ +// This file is generated automatically by `scripts/build/fp.js`. Please, don't change it. + +import fn from '../../addInTimeZone/index.js' +import convertToFP from 'date-fns/fp/_lib/convertToFP/index.js' + +var addInTimeZone = convertToFP(fn, 3) + +export default addInTimeZone