Mastering dates in JavaScript
How not to be frustrated by the insufficiencies of the Date object

One of the least satisfactory aspects of the JavaScript language is the management of the Date object, especially considering that manipulating dates is one of those things where the inexperienced programmer is less comfortable.
Why do I say this? The most fundamental missing pieces, in my opinion, are effective methods for:
- Formatting a date
- Transforming a string representing a date into a Date object
- Performing date arithmetics, e.g. calculating the distance between two dates or adding a hours, days, months, etc., to a given date
In the following sections, I will show how you can remedy these shortcomings in JavaScript with unnecessarily tricky solutions and how, on the contrary, a set of fantastic libraries allow you to achieve the same results straightforwardly.
What is a Date in JavaScript
A JavaScript Date is a built-in object that internally contains the number of milliseconds from Jan 1st, 1970, while externally it is presented with pretty formatted string, that depends on the environment. For instance, the statement:
let d = new Date;
Creates a Date object that contains the current date and time. While I’m writing this text, on Jun 21, 2020 at 2.27 pm, the corresponding Date contains an integer like 1592828905046. We can show this internal value with .valueOf
:
new Date().valueOf() is equal to 1592828905046
The same value (in the same instant) can be obtained with:
Date.now()
If we inspect a Date object with console.log() or document.write() we obtain a human readable string:
Mon Jun 22 2020 14:26:59 GMT+0200 (Central Europe daylight saving time) [Chrome, Firefox]orMon Jun 22 2020 14:26:59 GMT+0200 (CEST) [Safari]
Formatting a date with the Date object methods
Most of the programming languages invented in the last 30 years provide appropriate functions to format a date the way you need it, e.g. to show it on a web page. JavaScript does not.
For instance, let’s say we have a Date object mydate that we need to show as a text in the following ways:
1) June 22nd, 20202) 22/06/20203) 22.06.20 13.004) Tue, Jun 22, 1.00 pm
Using just ES5 JavaScript (with neither any library nor the Intl object, see next section), the situation is sad:
- “June 22nd, 2020” — is not doable: there is no way to obtain the full month name from JavaScript by itself (without Intl).
- “22/06/2020” — the shortest solution I can find is:
let d = mydate.toISOString(); // returns 2020–06–20T13:00:00.000Z
d = d.substr(0,10).split('-').reverse().join('/');
3. “22.06.20 13.00” — a (horrible) solution is:
let d = mydate.toISOString();
d = d.substr(8,2)+’.’+d.substr(5,2)+’.’+d.substr(2,2)+' '+d.substr(11,5).replace(':','.') ;
4. “Mon, Jun 22, 1.00 pm” — too complicated to deserve a quote: we can cut the various pieces from what we get from .toString() or toLocaleString() or .toUTCString() and recompose them together.
Formatting a date with the Intl.DateTimeFormat object
Aware of these shortcomings and looking for a broader answer to the problem of regional representations of dates, numbers, etc., ECMA International introduced in 2015 a specific API for internationalization which includes a solution to these problems, together with many other features.
Unfortunately, the solution found is far from being a simple solution. First of all, ECMA International chose not to augment the Date object with powerful new methods (is it such a silly idea?), but they invented a new Intl.DateTimeFormat object, which makes a .format method available for formatting dates. And the result is open to criticism.
Let’s see what I mean.
1) June 22nd, 2020
We still cannot directly get what we want, because Intl.DateTimeFormat provides no option for producing the ordinal number suffix. The best approximation we can get is this:
new Intl.DateTimeFormat(‘en-US’,{ year: ‘numeric’, month: ‘long’, day: ‘numeric’ }).format(mydate)
That gives June 22, 2020. Not a very compact solution, to say the least.
By the way, the Intl API does provide a way to produce the ordinal number suffix, within the Intl.PluralRules object, but there is no way to get the same effect within the Intl.DateTimeFormat.format method.
The second example can be exactly solved in a similar way:
2) 22/06/2020new Intl.DateTimeFormat('it-IT',{ day: '2-digit', month: '2-digit', year: 'numeric' }).format(mydate)
For the third case there is no compact solution, as we want a non standard separator and we need to remove the comma after the date:
3) 22.06.20 13.00let d = new Intl.DateTimeFormat('en-GB',{ day: '2-digit', month: '2-digit', year: '2-digit', hour:'2-digit', minute:'2-digit', hour12:true }).format(mydate);d = d.replace(/\//g,'.').replace(/:/,'.').replace(',','')
Our fourth example is correctly doable with Intl.DateTimeFormat:
4) Mon, Jun 22, 1.00 pmnew Intl.DateTimeFormat('en-US',{ weekday:'short', month: 'short', day: 'numeric', hour: 'numeric', minute:'2-digit', hour12:true }).format(mydate)
However, looking at the expressions above, does not a sense of despair assail you? Doesn’t it seem unnecessarily verbose?
Parsing a date
Let us continue with the second most frequent operation on dates a programmer deals with: parsing a string, that represents a date, to get the corresponding Date object.
Indeed, the Date “class” has a “.parse” method that is supposed to do just that:
Date.parse(‘2012–07–06’) gives 1341532800000andDate.parse(‘July 6, 2012’) gives 1341525600000
Wait, wait! But aren’t these the same dates? We got different numbers, what’s the hell? Let’s format them back:
new Date(Date.parse(‘2012-07-06’)) : Fri Jul 06 2012 02:00:00 GMT:+0200new Date(Date.parse(‘Jul 6, 2012’)) : Fri Jul 06 2012 00:00:00 GMT:+0200
Do you see the difference? The hour is 2:00 pm in the former and midnight in the latter. The JavaScript engine interprets two dates, that are equal for a human being, as if they were different: the first one is interpreted as the midnight in the GMT timezone, and converted to my time-zone (CEST), that makes it 2.00 am. The second date is interpreted as the midnight in my time zone.
I have tested them on Chrome, Firefox and Safari: they all agree on this interpretation, which means that they probably adhere to the official definition made by ECMA.
Isn’t that nonsense?
To make matters worse, the various JavaScript engines do not always agree with each other about how to interpret a string. For example:
Date.parse(‘2012-07-06 14:20:34’)
gives 1341584434000 in Chrome and Firefox
NaN in Safari
Safari is a purist and only recognises the date-time with the “T” separator between the date and the time, e.g. 2012–07–06T14:20:34.
In addition to this, the Date.parse method can only interpret very few string types: ISO format (our first example), long date (second example) and short date (e.g. 07/06/2020). There is no way to inform the .parse function about how the date’s components are placed in the string.
Date arithmetics
When dealing with dates, a frequent need is to calculate the time interval between two dates or to compute what date is n hours, days, months, etc. after or before a given date.
That is easy enough with native JavaScript, making arithmetical operations on the numeric value corresponding to a date, i.e. the integer representing the number of milliseconds since Jan 1, 1970.
For instance,
- Compute the difference in hours between date2 and date1:
Math.round( ( date2.valueOf() — date1.valueOf() ) / (60*60*1000) );
2. Compute the Date that is two weeks in the future since now:
new Date(Date.now() + 2*7*24*3600*1000);
3. Compute my age in days:
Math.floor((Date.now()-Date.parse(‘1956–07–13’)) / (24*3600*1000));
Not so difficult, but it is worth noting the lack of any direct and user-friendly method to do the same.
The problems that arise can often be more complicated:
4. Find the next Monday
let now = Date.now();
let today = now — now%(24*3600*1000); // round to current day
let weekDay = new Date(today).getDay(); // Sunday=0, Monday=1 etc.
let nextMonday = new Date( today + (1 + (7-weekDay)%7)*24*3600*1000 );
Libraries doing their due
Luckily, some JavaScript libraries exist that solve all problems created by the insufficient support that JavaScript gives for manipulating dates. And I would say that any decent JavaScript programmer must use one of them, instead of relying on the native JavaScript Date and Intl objects.
I’m going to show how we can brilliantly solve all examples listed above with one of the following libraries:
- moment.js, the best known and most complete library, supporting a wide range of browsers. It includes full support for internationalisation.
- day.js, a fast and lightweight alternative to moment.js (all internationalisation support is kept in separate files)
- luxon, the newest library, supported by the same organisation as moment.js, uses a different approach, with pro’s and con’s compared to moment.js (see Why does Luxon exist?)
Formatting a date with moment.js and friends
Here is how our four given examples of formatting work out.
Moment.js and day.js
Provided the mydate is a wrapper object obtained with moment(mydate) or dayjs(mydate), our formatting problems are easily solved:
1) June 22nd, 2020 mydate.format('MMMM Do, YYYY')2) 22/06/2020 mydate.format('DD/MM/YYYY')3) 22.06.20 13.00 mydate.format('DD.MM.YY HH.mm')4) Tue, Jun 22, 1.00 pm mydate.format('ddd, MMM D, h.mm a')
Just straightforward, isn’t it? The full lists of formatting options are available here and here.
Luxon
Once a DateTime object is created with
let mydate = luxon.DateTime.fromISO('2020-06-22')
Our four examples can be (almost) solved like this:
1) June 22nd, 2020 mydate.toFormat('MMMM d, y')
Note: no support for the n-th suffix2) 22/06/2020 mydate.toFormat('dd/MM/y')3) 22.06.20 13.00 mydate.toFormat('dd.MM.yy HH.mm')4) Tue, Jun 22, 1.00 pm mydate.toFormat('EEE, MMM d, h.mm a')
Note: AM/PM is always uppercase
The reason why not as many formatting options are available in Luxon is that it relies on the Intl objects and it inherits all strengths and weakness from them. Full specifications of the .toFormat options are available here.
Parsing a date with moment.js and friends
Moment.js and dayjs
moment('2012–07–06')
moment('2012–07–06', 'YYYY-MM-DD')
moment('Jul 6, 2020', 'MMM D, YYYY')dayjs('2012–07–06')
dayjs('2012–07–06', 'YYYY-MM-DD')
dayjs('Jul 6, 2020', 'MMM D, YYYY')all giveFri Jul 06 2012 00:00:00 GMT+0200
It is just the sort of thing we can expect from a parse function. Full specifications are available here and here.
Note that both moment.js and day.js interpret the missing time portion as midnight in the current timezone. If you want to parse or display a date-time in UTC, you can use moment.utc()
anddayjs.utc()
instead of moment()
anddayjs()
.
Luxon
luxon.DateTime.fromISO('2012-07-06')
luxon.DateTime.fromFormat('2012-07-06','y-MM-dd');
luxon.DateTime.fromFormat('Jul 6, 2012','MMM d, y')all give2012-07-06T00:00:00.000+02:00
Like moment.js and day.js, when no additional options are given, the date is interpreted at the midnight of the current time zone. Full specifications of the Luxon parsing are available here.
Date arithmetics with moment.js and friends
Here is how our four given examples of calculation with dates work out.
Moment.js and day.js
1. compute the difference in hours between date2 and date1: -> moment(date2).diff(date1,’hours’)
-> dayjs(date2).diff(date1,’hours’)2. compute the Date that is two weeks in the future since now:
-> moment().add(2,'weeks')
-> dayjs().add(2,'weeks')3. compute my age in days:
-> moment().diff('1956-07-13','days')
-> dayjs().diff('1956-07-13','days')4. find the next Monday
-> moment().startOf('isoWeek').add(1, 'week')
-> dayjs().startOf('isoWeek').add(1, 'week')
The last one needs some explanation: .startOf(‘isoWeek’) get the start date of the given period; in our example we need the start of the current week according with the ISO 8601 standard, i.e. Monday.
The full set of manipulation methods is explained here and here
Luxon
1. compute the difference in hours between date2 and date1: -> date2.diff(date1).as('hours')2. compute the Date that is two weeks in the future since now:
-> luxon.DateTime.local().plus({weeks: 2})3. compute my age in days:
-> luxon.DateTime.local().diff( luxon.DateTime.fromISO('1956-07-13') ).as('days')4. find the next Monday
-> luxon.DateTime.local().set({weekday: 1}).plus({weeks: 1})
Full specifications are available here.
Conclusion
It is not clear why ECMA has not put its hand to the JavaScript Date object specifications since 2011, although its functional deficiencies are evident. Fortunately, some third parties libraries can avoid us many headaches.
Probably the best choice right now is day.js, because it is lighter than moment.js, more compact than luxon. Compared to moment.js, it has the additional advantage that its dayjs wrapper object is immutable, that is, it cannot be changed unintentionally by applying methods such as .add or .subtract to it.