Post Status: Draft/Under Development
This post is being written in conjunction with a piece of work dealing with JavaScript dates.
Draft last updated: 21 February 2018
Things to remember about JavaScript Dates:
- JSON.stringify() in JavaScript will serialize a date to UTC.
- Month numbers start at 0 (zero) in JavaScript, not 1 (one).
- So, January is represented by 0, through to December being represented by 11.
- You need to remember this when using the Date.getMonth() method;
- And when creating a new Date() object with a constructor overload – e.g. var today = new Date(2018, 1, 21) to create a date representing 21 February 2018.
- A JavaScript Date object is internally represented as the number of milliseconds since 1970-01-01 00:00:00 UTC;
- But the object itself uses the local timezone of the system running it – e.g. In Melbourne, Australia it returns a date/time of UTC+1000 during Australian Easter Standard Time and UTC+1000 during Australian Easter Daylight Time (during daylight savings).
- If you try to manually calculate the current UTC time and set a Date object to that date/time, then the value of date and time is the same as UTC, however, the Date object still returns the local system timezone.
- The JavaScript Date.getTimezoneOffset() is non-intuitive and returns the opposite of the UTC timezone offset sign.
- For example: If the local timezone is UTC+1100 then getTimezoneOffset() returns “-660” (that minus 660 minutes).
- Why?
- The definition of the method is: “The getTimezoneOffset() method returns the time zone difference, in minutes, from UTC to current locale (host system settings).”
- In other words: it subtracts the local timezone offset (including the offset sign) from UTC.
- Example: UTC+1100 timezone = UTC – (660 minutes) = 0 – 660 = -660 (negative 600).
- Example: UTC-500 timezone = UTC – (-300 minutes) = 0 – (-300) = 300 (positive 300).
- If you’re wondering how 0 – (-300) makes a positive, that’s fundamental maths: subtracting a negative value from another value adds those 2 values together.
- The zero in the examples is the timezone offset of UTC.
With that in mind, consider this scenario:
- We have a system that stores date/times in the database as UTC.
- We have a web front-end that works with JavaScript date.
- We want to pass a date from the front-end to a server-side API that records the date in the database.
I’ve seen the following:
- A JavaScript Date variable representing the current local date and time is created:
-
var localNow = new Date();
-
- We want to JavaScript Date variable representing a ‘StartDate’, and we want to assign the current UTC date/time to it:
-
var startDate = new GetUTCDate(localNow);
- See below for the GetUTCDate() function.
-
- We need to serialize startDate to JSON so send it over the wire to the API.
The problems with this are:
- ‘startDate’ may have a value that represents the UTC equivalent of ‘localNow’ but the actual ‘startDate’ JavaScript object still retains the local timezone offset.
- The JSON.stringify() method used to serialize a JavaScript object to JSON converts Date values to their UTC equivalent time (remember the internal representation of a Date object is an offset representing a UTC time);
- So what ends up happening is the ‘startDate’ value represented in the JSON is actually the UTC time plus or minus the local timezone offset.
- For example:
- If the current local time is 21 February 2018 at 10:00am, then ‘the startDate’ JavaScript object has the value: “2018-02-21 10:00:00.00000 UTC+1100”
- But the string/value for returned by JSON.stringify(startDate) is: “2018-02-20 23:00:00.00000 UTC” (11 hours earlier).
The following is written in a TypeScript:
// Takes a Date set to the local date/time, and return a Date representing the equivalent UTC date/time. public static GetUTCDate(localDate: Date): Date { //UTC+1100 returns -660 (negative). Remove offset from local time to reach UTC. //However, [local - (-offset)] = [local + offset], therefore equation is [local + (-offset)] which = [local - offset] //UTC-1100 returns 660 (positive). Add offset to local time to reach UTC. // NOTE: This still produces a Date object with local timezone offset let offsetMinutes = Math.abs(localDate.getTimezoneOffset()); if (localDate.getTimezoneOffset() < 0) { //Ex: GMT+1100 return new Date(localDate.getTime() - (offsetMinutes * 60000)); } else { //Ex: GMT-1100 return new Date(localDate.getTime() + (offsetMinutes * 60000)); } }
Running Example
If you’re using Internet Explorer or there’s just a black window below, here’s the link to the code (and I suggest using a “modern” browser – Chrome and Edge work): https://stackblitz.com/edit/js-date-gotchas.
Other things to consider:
- If you want to store just a date (no time component) in a database, you cannot perform any locale conversions if it is stored as UTC.
- Why? Let’s assume you want to convert the UTC date to the date in Timezone UTC+1100 AEDT (Melbourne, Australia). There are actually 2 dates this can convert to. To understand, look at it from the point of view of the local time we want to convert to:
- In Melbourne, for any local time from midnight to 10:59:59am the equivalent UTC time is the day before (e.g. if the local date is 21 February, then UTC date is 20 February);
- However, for the remainder of the day – 11:00am to 23:59:50pm – the UTC date is the same as the local date (i.e. both dates are 21 February).
- Why? Let’s assume you want to convert the UTC date to the date in Timezone UTC+1100 AEDT (Melbourne, Australia). There are actually 2 dates this can convert to. To understand, look at it from the point of view of the local time we want to convert to:
- Think about how you want to handle dates that are timezone neutral. That is a date that remains the same regardless of where in the world you are. For example, a date of birth usually does not care about where in the world the person is born, nor the timezone.