Mastering dates in JavaScript

How not to be frustrated by the insufficiencies of the Date object

Giorgio D.F.
Jul 5, 2020 · 8 min read
Image for post
Image for post
Photo by Jess Bailey on Unsplash

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:

  1. Formatting a date
  2. Transforming a string representing a date into a Date object
  3. 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:

  1. “June 22nd, 2020” — is not doable: there is no way to obtain the full month name from JavaScript by itself (without Intl).
  2. “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,

  1. 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 suffix
2) 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.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store