Create Relative Timestamps in JavaScript Without Any Dependencies Using `Intl.RelativeTimeFormat`

My Attempt on creating a relative timestamps (like "5 min ago", "4d ago", "in 1 week", etc.) without any external dependencies

By Muhammad Rizqi Ardiansyah on 2023-05-20

Summary


Handling dates can be cumbersome in general. Especially in JavaScript, the built-in Date class isn't the most convinient at handling dates. So, what I've seen is that a lot of JavaScript developers tends to use external libraries/package.

But developers aren't exactly the wisest at choosing the package. One of the most popular libraries for date handling in JavaScript, moment, has been recommended by the developers to use something else, due to various reasons. This package isn't exactly small either. According to bundlephobia, it's bundle size is 290.4kb as of May 2023. You can take a look at it here. All these reasons to not use this particular package, yet I've seen some developers start a new project using this package.

Fortunately, though, there have been an alternative of moment package. My most favorite one is dayjs. Not only it's similar to moment syntax-wise, it's also the lightest one among other alternatives.

But I think we can do better, how about we avoid installing any dependencies and instead create our own implementation of relative timestamp by relying on JavaScript's built-in class--therefore we don't have to think about dependencies and increased bundle size.

Create a relative timestamps (like "in 4 min." etc) using built-in Intl.RelativeTimeFormat class

The documentation for this class can be read here.

First, we need instantiate the class with locales and some options, like this:

const relativeTimeFormat = new Intl.RelativeTimeFormat("en-US", {
  // "always", or "auto"
  numeric: "auto",
  // "long", "short", or "narrow"
  style: "long",
});

We can then generate a relative timestamps using the method format, like this:

relativeTimeFormat.format(-10, "month"); // "10 months ago"
relativeTimeFormat.format(20, "day"); // "in 20 days"

At this point, we already have a reliable and nicely relative timestamp generator, that will also generate the timestamp based on the desired locale too. But there's some drawbacks. What if, hypothetically you use the unit day for the formatter, but the input to the formatter is 15 years, or in day unit, 5478 days?

relativeTimeFormat.format(-5478, "day"); // "5,478 days ago"

Clearly, this is a bit hard to read. Personally, I prefer reading "15 years" instead of reading "5,478 days ago" then calculating it using my potato-brain how many years is that.

Automatically deciding the unit based on the input

Unfortunately, the format method doesn't have that responsibility, so we'd have to create our own logic.

We'll need these to get our logic up and running:

Before we start, let's define some constants to make our life easier:

const ONE_MILLISECOND = 1;
const ONE_SECOND = 1000 * ONE_MILLISECOND;
const ONE_MINUTE = 60 * ONE_SECOND;
const ONE_HOUR = 60 * ONE_MINUTE;
const ONE_DAY = 24 * ONE_HOUR;
const ONE_WEEK = 7 * ONE_DAY;
const ONE_MONTH = 30 * ONE_DAY;
const ONE_YEAR = 12 * ONE_MONTH;

Let's create the first function:

export function getUnit(inputTime: Date, currentTime: Date) {
  const timeDiff = Math.abs(currentTime.getTime() - inputTime.getTime());

  if (timeDiff < ONE_MINUTE) return "minute";
  if (timeDiff < ONE_HOUR) return "minute";
  if (timeDiff < ONE_DAY) return "hour";
  if (timeDiff < ONE_WEEK) return "day";
  if (timeDiff < ONE_MONTH) return "week";
  if (timeDiff < ONE_YEAR) return "month";

  return "year";
}

Then let's create the function to return the correct value:

export function getCorrectTimestampValue(inputTime: Date, currentTime: Date) {
  const timeDiff = currentTime.getTime() - inputTime.getTime();

  if (timeDiff < ONE_MINUTE) return Math.floor(timeDiff / ONE_MINUTE);
  if (timeDiff < ONE_HOUR) return Math.floor(timeDiff / ONE_MINUTE);
  if (timeDiff < ONE_DAY) return Math.floor(timeDiff / ONE_HOUR);
  if (timeDiff < ONE_WEEK) return Math.floor(timeDiff / ONE_DAY);
  if (timeDiff < ONE_MONTH) return Math.floor(timeDiff / ONE_WEEK);
  if (timeDiff < ONE_YEAR) return Math.floor(timeDiff / ONE_MONTH);

  return Math.round(timeDiff / ONE_YEAR);
}

Finally, we can call both function to create a better relative timestamps:

export function relativeTimeFormatter(
  inputTime: Date,
  currentTime: Date = new Date()
) {
  return relativeTimeFormat.format(
    getCorrectTimestampValue(inputTime, currentTime),
    getUnit(inputTime, currentTime)
  );
}

Hope this helps,

Rizqi