I read someone’s question on SO recently asking “How do I add a day to my date?”, and he received a barrage of suggestions, many arguing that with previous posters that their solution would fail for reasons such as "it won't handle daylight-savings".
Well it occurred to me that there’s really not enough information in the original question to give a proper answer, and it’s not only a classic example of the importance of separating data from presentation information, but also an excellent example of understanding requirements and potential mishaps in seemingly simple situations.
Firstly, it’s important to note that 24 hours later is not always the same as “this time tomorrow“. Daylight savings causes some quite serious problems, because one day each year we have no times existing between 2am and 3am, and six months later, we have the same hour occur twice on the same day!
So what if I set a backup to run at 2:30 am every day? What will happen on 1st April here in Melbourne? Well, if we rely on local time format, the backup will execute twice.
So what about 7th October, 2018? Our backup will never run, because 2am-3am ceases to exist.
OK, so this is a bit of a simplistic set of statements, because we know we would look to see if it was after 2:30am, so that would probably fix 1st April, but even then, the double whammy on 7th October is an issue.
The fact is that DST handling is a presentation factor, not a data one. The underlying data value in a Date
object is an integer with a count of the number of milliseconds since midnight on 1/1/1970. But anything we display or parse using the Date()
object will by default be handled in local time, unless we specifically tell it to use UTC (GMT) timezone. So just be sure to differentiate between UTC and local times.
All maths should be done on the UTC time, and all presentation can be done on local times where Date
(or moment.js/date-fns) converts the date (for leap years) and hours (for DST), and even 30 minutes for timezones on half-hour adjustments.
Working in local time introduces some rather serious issues. Since the clock goes forward for us on 7th October, 2018 at 2am, what happens if I try to set Date()
to be 2:30am? Well, you get a rather confusing behaviour:
new Date('2018-10-07T01:30:00')
Sun Oct 07 2018 01:30:00 GMT+1000 (Local Standard Time)
new Date('2018-10-07T02:30:00')
Sun Oct 07 2018 03:30:00 GMT+1100 (Local Daylight Time)
new Date('2018-10-07T03:30:00')
Sun Oct 07 2018 03:30:00 GMT+1100 (Local Daylight Time)
Performing the same type of manipulation with UTC times shows the relationship between the UTC time and local time properly (notice the Z on the end to indicate UTC or GMT):
new Date('2018-10-06T15:30:00Z')
Sun Oct 07 2018 01:30:00 GMT+1000 (Local Standard Time)
new Date('2018-10-06T16:30:00Z')
Sun Oct 07 2018 03:30:00 GMT+1100 (Local Daylight Time)
new Date('2018-10-06T17:30:00Z')
Sun Oct 07 2018 04:30:00 GMT+1100 (Local Daylight Time)
But, back to the original question:
How Do I Add a Day?
Well, do I want to add 24 hours, or one day?
One day sounds simple, but what if it’s 2:30am March 31st? Which of the 2:30ams do I set to the next day? It’s critical to understand the usage of time periods in applications, and it’s advisable to use relative times wherever possible, as they are fixed numeric deltas that can be applied to the UTC representation of the current time, regardless of timezone.
So essentially, make sure you always store and manipulate data using UTC based functions, and not local time, otherwise, you open yourself up to a whole world of confusion and pain.
I always recommend people persist all data in UTC format, and supply all data to all objects in UTC format, allowing those objects and/or views to render those times in whatever format they require.
Use a library such as momentjs
or date-fns
to simplify manipulating your dates, but beware of code bloat as some of these libraries include all the locale data and string
s, so make sure you trim them down to the locales you require.
If you store a users timezone at the server end to allow your client to display data with the correct local value, make sure you store it with sufficient resolution. In Australia, we have several timezones, some of which have daylight savings and some don't, so it's no good storing an hour offset (e.g. GMT+10) as an adjustment factor, as the offset in Victoria (which has daylight savings) is sometimes GMT+11, while it's still only GMT+10 in Queensland.
Instead, find a library such as moment-timezone that allows you to convert to and from other named timezones, and store the name of the timezone against a users profile. This can be a little tricky at the server-end, as frameworks such as .NET have a different set of timezone names to moment-timezone, and .NET Core applications running on Linux use a completely different set of timezone names to those on Windows.
Finally, please design your timezone related functionality into your application from day one. Retro-fitting timezone features can be a nightmare, and you must ensure that all of your server-side functionality has also been designed with multiple timezones in mind, such as using GETUTCDATE()
for default values on SQL server instead of GETDATE()
.
Hopefully, this gives a few ideas on how best to approach the situation where a client application may be used by people in different timezones, and will hopefully provide a few tips on how to ensure your data store is timezone independent.