Recently, I was working on some GraphQL data fetching tutorial. The fetched data included time in Unix time format. The author proceeded with 10 minutes writing, in my opinion, unnecessary datetime processing function, to simply display the date output in the certain string format. The function would convert the timestamp to new Date(), extract year, month (which was also converted to display month name based on the corresponding number), date, hour, etc. as variables, and then finally, display it using template literals. The author also used "86400" seconds in adding dates (24 hours, 60 minutes, 60 seconds).
In my experience with Unix time, or in particular Timestamps used in Google Firestore, it is better to avoid writing these type of functions, especially, while dealing with time sensitive data, operations like addition and subtraction of time periods, or dealing with the timezones and daylight saving times.
There are libraries available, like moment-timezone, that make this type of operations easy, and reduce the chance of introducing unnecessary bugs.
Initial Setup:
Let’s take a look at the implementation of moment-timezone in the examples below. For the simplicity, let’s create a folder, make sure that we have node and npm installed on the system, and let’s initialize package.json with default settings so we can install moment-timezone. In your terminal:
> mkdir date-manipulation
> cd date-manipulation
> npm init -y
> npm install --save moment-timezone@0.5.28
I am using node v8.12.0, and npm v6.4.1 for this example.
Dates and Moment-Timezone:
In the same folder, let’s create a new file, call it datelogger.js and add the code below:
const dateTime = 1388620296020;
console.log(new Date(dateTime));
When we run this code in the terminal, we get the timestamp converted to JavaScript Date instance, which then is printed in the console. The timestamp is timezone independent.
> node datelogger.js
2014-01-01T23:51:36.020Z
- Note: Running the same code in the browser or codesandbox will output it in the local timezone.
The output is a date in ISO 8601 format, with T being the time designator preceding the time component, and Z is the zone designator for the zero UTC offset.
So how can we display this date in the local or required timezone? moment_timezone comes to help in these situations:
const moment = require('moment-timezone');
const dateTime = 1388620296020;
console.log(moment.tz(dateTime, 'America/Chicago').format());
Output:
2014-01-01T17:51:36-06:00
The time is now shown in -06:00 zone with the hours changing accordingly.
The time can be also formatted to match the needed output:
console.log(
moment.tz(dateTime, 'America/Chicago').format('MM/DD/YYYY HH:mm:ss')
);
console.log(
moment
.tz(dateTime, 'America/Chicago')
.format('MMMM DD, YYYY [at] HH:mm [and] ss [seconds, timezone:] z')
);
Output:
01/01/2014 17:51:36
January 01, 2014 at 17:51 and 36 seconds, timezone: CST
As it is seen in the output above, the additional text can be added to the format() using square brackets [ ].
Adding and Subtracting Days:
Let’s say we are working on a timeline app and dates of the project are adjusted by either addition or subtraction. The naive approach would be taking the amount of days and adding the equivalent seconds to the timestamp. For this example let’s assume that the project is delayed for 5 days:
const delayDays = 5;
const adjustedTime = dateTime + delayDays * (24 * 60 * 60 * 1000);
Output:
2014-01-01T23:51:36.020Z
2014-01-06T23:51:36.020Z
As we can see, the days are correctly adjusted, and the output shows January 6th. But what happens if we are in daylight saving time observing zone and adding or subtracting during the transition?
const givenDate = 1583042400000;
console.log(new Date(givenDate));
console.log(moment.tz(givenDate, 'America/Chicago').format());
So far, ok! We are in the Central zone, and the date is correctly showing midnight. Let’s add enough days, so we pass Sunday, March 8, the beginning of daylight saving time:
const addedDays = 10;
const givenDateNoDST = givenDate + addedDays * (24 * 60 * 60 * 1000);
console.log(moment.tz(givenDateNoDST, 'America/Chicago').format());
Output:
2020-03-11T01:00:00-05:00
As we see above, the timezone is correctly displaying as -05:00 instead of -06:00. But, there is an issue with displaying the hours. It is showing 1AM instead of midnight. If we need to display the data from midnight to midnight, the data will be shifted an hour, and parts of the data will be missing for the corresponding day. To avoid such behavior introduced by naive approach, we can implement something similar to the below function or use moment-timezones available .add() or subtract():
Writing a function:
const addDays = (dateVar, days) => { const date = new Date(dateVar); date.setDate(date.getDate() + days); return date; }; console.log(moment.tz(addDays(givenDate, 10), 'America/Chicago').format());
Output:
2020-03-11T00:00:00-05:00
Using moment-timezone:
console.log(moment.tz(givenDate, 'America/Chicago').add(10, 'days').format());
Output:
2020-03-11T00:00:00-05:00
As it is shown above, in both methods the timezone is displayed correctly while preserving the hours.
Summarizing, dealing with time is not as easy as it seems. We’ve all done calculations like "24 x 60 x 60" before, and no one is safe from occasional mistakes that be potentially dangerous. It is a good practice, if time permits, to take a break, and then go over the code with a fresh mind, and check what exactly the code does. There are also libraries like moment-timezone that can save us some head ache.
For more reading on the subject, I would suggest a blog post by @swizec, where he describes the common pitfalls dealing with time, timestamps, and DST.
PS. I haven’t blogged in a while. It was refreshing to put some thoughts in a written format, though it took me about 3.5 hrs to put together this post. I believe that it will get better by time. And now back to learning! 😀 ?