Adding days to a date depending on time zones and DST (Node.js)

GSanchez
Trucksters
Published in
6 min readMar 23, 2023

Working with timezones can often be a pain for many developers. We are running our server in one timezone, we are developing in another one, and our users can access our platform from anywhere in the world. Handling all this can be difficult for some business areas. Working only in UTC when storing them can help, but then, daylight saving time appears, and things just get worse.

https://www.reddit.com/r/ProgrammerHumor/comments/drjwz2/everyone_complains_about_time_zones_but_not/
https://www.reddit.com/r/ProgrammerHumor/comments/drjwz2/everyone_complains_about_time_zones_but_not/

Daylight Saving Time (DST), also known as summer time is a strategy follow by some countries with the aim of saving energy. This is done by advancing one hour in all clocks during summer months. This way, people could take advantage of the first light hours of the day during those months. Initially proposed to reduce candles usage, it’s still used in most parts of Europe and North America.

Source: Wikipedia. Blue: Countries still using DST

This should be no issue in terms of software engineering, since most modern operating systems manage dates in UTC (Coordinated Universal Time). This format represents an unequivocal amount of time, valid everywhere. Then, every issue regarding time zones is just a matter of proper visualization of this universal date. For example, UTC 00:00:00 1st Jan 2023 would be 16:00:00 31st Dec 2023 in San Francisco (Pacific Standard Time) or 11:00:00 1st Jan 2023 in Sydney (Australian Eastern Daylight Time). Those tree dates represent the exact same moment.

If we had to handle DST, it will not be much of a pain if we have our dates stored in universal time. It will be just a matter of presenting it properly depending on the time zone we are. If we are in DST, just subtract one hour and that’s all, we are now in summer time!

Moreover, most modern programming languages and libraries will automatically handle DST. In case of Node.js, we are also covered. To better understand how this is working, let’s propose an example.

Imagine we are working in a maintenance business. We want a program to store an object containing some information regarding some jobs we are doing. We call this, templates, and they would have the following structure:

interface maintenanceJob {
clientName: string,
clientPhone: string,
clientAdress: string,
price: number
scheduledAt: Date
}

This object help us to store the information regarding a maintenance job at a specific client site in a specific time slot.

Now imagine that we are performing the same maintenance job in the same client company every month. Since those jobs can be recurrent, we would like a method to replicate the maintenanceJob object, but with a new scheduledAt date. Let's say, 90 days after the first one.

// Running the code on February in a local development machine
// in 'Europe/Madrid' timezone

function addDaysToDate(baseDate: Date, daysInterval: number) {
var newDate = new Date(baseDate)
newDate.setDate(newDate.getDate() + daysInterval)
return newDate
}

const februaryDate = new Date('2022-02-14 15:45:00.000 +0100')
// 14 February 2022 at 15:45:00 CET
// 14 February 2022 at 14:45:00 UTC

const mayDate = addDaysToDate(februaryDate, 90)
// 15 May 2022 at 15:45:00 CEST
// 15 May 2022 at 13:45:00 UTC

Note how Node.js setDate method has automatically subtracted one hour so that both dates, February and May one, have the same local value. If we compare the UTC values, we can clearly see how one hour was subtracted. This is the behaviour we desire, since we want the same local hour.

So far, so good. This code is ready for production. But, after a successful deployment, and after executing the previous piece of code in the cloud, we get the following:

// Running the code in February in a cloud machine

function addDaysToDate(baseDate: Date, daysInterval: number) {
var newDate = new Date(baseDate)
newDate.setDate(newDate.getDate() + daysInterval)
return newDate
}

const februaryDate = new Date('2022-02-14 15:45:00.000 +0100')
// 14 February 2022 at 15:45:00 CET
// 14 February 2022 at 14:45:00 UTC

const mayDate = addDaysToDate(februaryDate, 90)
// 15 May 2022 at 16:45:00 CEST <- This local hour now differs!
// 15 May 2022 at 14:45:00 UTC

Note how now, the May date has not that one hour subtracted! Now, both dates will not represent the same hour in our local timezone. If we were to use this new maintenanceJob object, we would be one hour late at ours client site!

We could think that this issue could be better handled by some more libraries.

import { addDays } from 'date-fns'
function addDaysToDateDateFns(baseDate: Date, daysInterval: number) {
return addDays(baseDate, daysInterval)
}

import moment from 'moment'
function addDaysToDateMoment(baseDate: Date, daysInterval: number) {
return moment(baseDate, 'DD-MM-YYYY').add(daysInterval, 'days').toDate()
}

import date from 'date-and-time'
function addDaysToDateDateAndTime(baseDate: Date, daysInterval: number) {
return date.addDays(baseDate, daysInterval)
}

But, after some trial-error, the result would be equivalent when executing our program in the cloud.

Then, what’s happening? Why are we failing when executing our program in the cloud?

We could get a hint if we try to execute the code for a timezone which doesn’t have DST in our local development machine. If our company now works for a client located in Iceland (no DST here), but we still execute our code in ‘Europe/Madrid’ :

// Running the code on February in a local development machine
// in 'Europe/Madrid' timezone

function addDaysToDate(baseDate: Date, daysInterval: number) {
var newDate = new Date(baseDate)
newDate.setDate(newDate.getDate() + daysInterval)
return newDate
}

const februaryDate = new Date('2022-02-14 15:45:00.000 +0000') // ICE timezone
// 14 February 2022 at 15:45:00 GMT
// 14 February 2022 at 15:45:00 UTC

const mayDate = addDaysToDate(februaryDate, 90)
// 15 May 2022 at 14:45:00 GMT
// 15 May 2022 at 14:45:00 UTC

In this case, even though Iceland doesn't follow the DST, we are compensating it!

What’s happening is that Node.js is taking into account the local value to perform the addition or subtraction of that DST hour, when adding days. This is performed by reading the local time zone where the code is being executed. Usually, this is stored in the TZ environment variable.

So, what could we do to solve this? Apparently, we would need to run the code in the same timezone we would like to apply the addition of the days, but if we have everything set up in the cloud, this would mean we would never compensate DST time zones. This is, obviously, non-feasible.

So, we would need to work out by our own the method to accomplish this.

If we want to be timezone agnostic, we need to manually perform the addition of the corresponding milliseconds to the target day. Then, depending on the timezone we are interested in, we could compensate the DST hour. To help with that, we can use getTimezoneOffset, which will tell us the offset between a date with a specific timezone and the UTC.

import { getTimezoneOffset } from 'date-fns-tz'

function addDaysByTz(baseDate: Date, daysInterval: number, timezone: string): Date {
const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24
const MILLISECONDS_PER_HOUR = 1000 * 60 * 60

const baseDateHourUtcOffset = getTimezoneOffset(timezone, baseDate) / MILLISECONDS_PER_HOUR
const newDate = new Date(baseDate).getTime() + MILLISECONDS_PER_DAY * daysInterval
const newDateHourUtcOffset = getTimezoneOffset(timezone, newDate) / MILLISECONDS_PER_HOUR

const seasonalOffset = baseDateHourUtcOffset - newDateHourUtcOffset
const newDatesCompensated = new Date(newDate).getTime() + MILLISECONDS_PER_HOUR * seasonalOffset

return new Date(newDatesCompensated)
}

// ICE timezone
const februaryDateIce = new Date('2022-02-14 15:45:00.000 +0000')
// 14 February 2022 at 15:45:00 GMT
// 14 February 2022 at 15:45:00 UTC

const mayDateIce = addDaysByTz(februaryDate, 90, 'Iceland')
// 15 May 2022 at 15:45:00 GMT <- Iceland date is NOT compensated
// 15 May 2022 at 15:45:00 UTC <- Iceland date is NOT compensated


// Madrid timezone
const februaryDateIce = new Date('2022-02-14 15:45:00.000 +0100')
// 14 February 2022 at 15:45:00 CET
// 14 February 2022 at 14:45:00 UTC

const mayDateIce = addDaysByTz(februaryDate, 90, 'Europe/Madrid')
// 15 May 2022 at 15:45:00 CEST <- Madrid date is compensated
// 15 May 2022 at 13:45:00 UTC <- Madrid date is compensated

In fact, having to state which timezone we want to add the days makes sense if we want to actually differentiate between DST and non-DST timezones.

Hope this piece of code can help!

Did you find this interesting?

At Trucksters, we are always facing new problems and challenges. Visit our Careers' homepage to check the latest open positions! 👇

--

--