Moment.js Without the Dependencies

I've always liked Moment.js, ever since its first release back in 2011 (geeze is it really that old?). It's a more natural way of reading dates for me, much easier for me to understand. I don't have to look at a date and think “how long ago was that again?”, the date tells me: “Last Friday”, or “2 months ago”, or “5 years ago”.

Well I’m here to let you know that no longer do you need a library to get this functionality! JavaScript now supports this sort of thing out-of-the-box, through the magic of the Intl object.

The Intl Object #

Per the MDN:

The Intl object is the namespace for the ECMAScript Internationalization API, which provides language sensitive string comparison, number formatting, and date and time formatting.

It’s the “date and time formatting” that interests us, but it’s worth noting that there’s a lot more you can do with this object.

How comprehensive is this date and time formatting? Not very comprehensive, it turns out. But it’s comprehensive enough, it does enough to allow us to change it to fit our needs, but no more. Let me show you what I mean.

Using the Intl object #

The Intl object is all about internationalization, so before we do anything we need to tell it what language we will be using. Here’s a snippet to make it nice and easy:

var timeFormatter = new Intl.RelativeTimeFormat(navigator.language);

navigator.language returns the user’s prefered language, so it is perfect for our purposes.

Now that we have our formatter, there are a few options we can tap into. The formatter wants to know two things: (1) should I always include a number? and (2) how much space can I take up?

If you say no to #1, then the formatter will occasionally substitute words for numbers. Instead of “0 days ago” or “1 day ago” it will say “today” and “yesterday” (or the international equivalent).

As for option #2, you can tell the formatter whether you want abbreviations or not. You can choose between “in 1 month” and “in 1 mo.”

Here’s how you can code each of those things:

// allow "yesterday" and "tomorrow" and things like that
var allowAdverbs = new Intl.RelativeTimeFormat(navigator.language, { numeric: 'auto' });
// don't allow those things (also the default)
var noAdverbs = new Intl.RelativeTimeFormat(navigator.language, { numeric: 'always' });

// no abbreviations
var noAbbr = new Intl.RelativeTimeFormat(navigator.language, { style: 'long' });
// some abbrivations
var someAbbr = new Intl.RelativeTimeFormat(navigator.language, { style: 'short' });
// ALL THE ABBREVIATIONS
var allAbbr = new Intl.RelativeTimeFormat(navigator.language, { style: 'narrow' });

So those are our options. Now for the actual implementation.

Implementation of RelativeTimeFormat #

If you’ve ever used dates in JavaScript, you know that they’re hopelessly complicated and hard to use. This time formatter doesn’t fix that, but it does help. We still need to jump through some hoops to get this to work for us, but once you understand the method, it shouldn’t be too difficult.

The first thing you need is a relative date, because we’re working with a relative time formatter. With JavaScript you can get a relative date by subtracting two dates, so we can subtract our target date from our current date, and that will give us our relative date. In practice, that looks like this:

var targetDate = new Date(Date.UTC(2019, 06, 03));
var now = new Date();
var relativeDate = date.getTime() - now.getTime();
// now we have our relative date in milliseconds

Next we need to convert our relative date to something that our formatter understands. In order to do this, I’m going to write a little helper function, so we can access any unit of time that we want. Our helper function will look like this:

function convertMilliToUseable(milliseconds) {
return {
seconds: Math.round(milliseconds / 1000),
minutes: Math.round(milliseconds / 1000 / 60),
hours: Math.round(milliseconds / 1000 / 60 / 60),
days: Math.round(milliseconds / 1000 / 60 / 60 / 24)
}
}

This function takes our millisecond count and converts it into units that we can use, aka seconds, minutes, hours, and days.

Now that we have useable units, the last thing is to use them! We can drop those units into our formatter to get whatever units we want:

console.log(formatter.format(units.days, 'days'));
// returns "12 days ago"

Taking the next step #

RelativeTimeFormat doesn’t have as many options as Moment.js. As you can see above, the customizability is pretty sparse. Plus it’s not nearly as smart, RelativeTimeFormat only spits out whatever text you tell it to spit out, it doesn’t dynamically choose the closest unit of time, like Moment does.

Fortunately, we can pretty easily add that functionality ourselves. Here is a basic implementation, to show you how we could do this:

var start = new Date(Date.UTC(2018, 01, 23));
var relativeString = convertDateToRelativeString(start);
// relative string will return anything from "12 seconds ago"
// to "last year" or "5 years ago"


function convertDateToRelativeString(start) {
const now = new Date();
const timeDiffInMil = start.getTime() - now.getTime();

var formatter = new Intl.RelativeTimeFormat(navigator.language, { numeric: 'auto' });

// configuring the different options we want our formatter to go through
let possibleUnits = ['minute', 'hour', 'day', 'week', 'year'];
// 60 seconds in a minute, 60 minutes in an hour, 24 hours in a day, and so on
let possibleValues = [60, 60, 24, 7, 52];

let unit = 'second';
let value = timeDiffInMil / 1000;

// Now we loop through all of our options, find the option that is the closest to our date, and ignore the rest
for (var i = 0; i < possibleUnits.length; i++) {
if (Math.abs(value) > possibleValues[i]) {
value /= possibleValues[i];
unit = possibleUnits[i];
}
}

return formatter.format(Math.round(value), unit);
}

In this code, the convertDateToRelativeString does a series of comparisons on our date. The logic is pretty basic: If the date is more than 60 seconds past, it will display minutes. If the date is more than 60 minutes past, it will display hours. And so on and so forth. We can configure this function to use whatever time scale you want.

This method is not as simple as calling a function in Moment, but it's a whole lot faster, and it doesn't require keeping dependencies up-to-date.

Browser Support #

Again per the MDN, this feature is not yet supported in Edge, IE, or Safari. But if done properly, it should fall back to a regular date pretty easily. I very much view this as a progressive enhancement, something that you can turn on if it's supported, or you can go with regular dates.

Summary #

As with all things programming, you always have to consider the pros and cons of whatever tools you decide to use. In the case of Moment.js vs. a custom solution, you have to decide if your needs are great enough to include a library. Libraries always have downsides associated with them, but they often have some considerable perks as well. Libraries are almost always slower than native solutions, but they're generally well documented, with a clean API, and a community of likeminded developers around them.

For myself, I always try to favor user experience over developer experience, so if I can easily get away with speeding up a website by not using a library, then I will often times do that. As I have shown in this blog post, it's not too difficult to roll your own basic Moment.js functionality, and I know that I, for one, will be coming back to this solution many times in the future. Your mileage may vary, but I for one am glad that I now have the tools to do this on my own.

← Home

Changelog
  • Add inclusive lang checker, remove non-inclusive language from blog posts
  • Adds browser support to latest article
  • Renames and publishes new blog post