"37 seconds ago"

Occasionally we want to show our users friendly relative time differences, just like that.

It seems simple enough. In fact, it is simple enough.

So simple that I thought I might check npm to see if someone else had done it and, of course, they have! However, instead of a little easy-to-copy gist, it's at an almost laughable level of enterprise complexity.

Do you think you really need enterprise-grade software to do this?

ago(Date.now() - happenedAtMs);
"37 seconds ago"

Now, there are some time functions that would seem simple and end up pretty hairy, but this isn't one of them. On the contrary, the time ago / from now combo is one of the easiest time functions you'll ever write!

Show me the magic!

The basic premise of that - a "time ago" or "from now" type of libary - the secret if you will, is repeating blocks of checking that a number is less than an upper limit (such as 1 second, 60 seconds, 3600 seconds, etc).

For example, the crudest of calculations might be represented like this:

function ago(ms) {
  if (ms < 1000) { return "moments ago"; }
  if (ms < 60 * 1000) { return "seconds ago"; }
  if (ms < 60 * 60 * 1000) { return "minutes ago"; }
  return "forever ago";
}

If you want to include "from now" you can reverse the sign:

  if (ms < 60 * 60 * -1000) { return "hours from now"; }
  if (ms < 60 * -1000) { return "minutes from now"; }
  if (ms < 0) { return "seconds from now"; }
  if (ms < 60 * 1000) { return "seconds ago"; }

If you want to fine tune it a little further, you do some simple subtraction:

  var unit = 0;
  var MINUTE = 60 * 1000;

  // handle the singular unit
  if (ms < 2 * MINUTE) { return "a minute ago"; }

  // handle plural untis, up to the next single unit (an hour)
  if (ms < 60 * MINUTE) {
    while (ms >= MINUTE) { ms -= MINUTE; unit += 1; }
    return unit + " minutes ago";
  }

  // lump everything else into hours
  return "hours ago";

Not a "Library", but a Scaffold

When something is this small (and especially when you might want to make tweaks that don't even make sense to try to "DRY" up or modularize), this Go Proverb comes to mind:

"A little copying is better than a little dependency" - Rob Pike's Go Proverbs

For that reason I grant here two variations of "time ago", triple licensed under the MIT, ISC, and Apache-2.0:

Time Ago in fewer than 50 lines:

'use strict';
function timeago(ms) {

  var ago = Math.floor(ms / 1000);
  var part = 0;

  if (ago < 2) { return "a moment ago"; }
  if (ago < 5) { return "moments ago"; }
  if (ago < 60) { return ago + " seconds ago"; }

  if (ago < 120) { return "a minute ago"; }
  if (ago < 3600) {
    while (ago >= 60) { ago -= 60; part += 1; }
    return part + " minutes ago";
  }

  if (ago < 7200) { return "an hour ago"; }
  if (ago < 86400) {
    while (ago >= 3600) { ago -= 3600; part += 1; }
    return part + " hours ago";
  }

  if (ago < 172800) { return "a day ago"; }
  if (ago < 604800) {
    while (ago >= 172800) { ago -= 172800; part += 1; }
    return part + " days ago";
  }

  if (ago < 1209600) { return "a week ago"; }
  if (ago < 2592000) {
    while (ago >= 604800) { ago -= 604800; part += 1; }
    return part + " weeks ago";
  }

  if (ago < 5184000) { return "a month ago"; }
  if (ago < 31536000) {
    while (ago >= 2592000) { ago -= 2592000; part += 1; }
    return part + " months ago";
  }

  if (ago < 1419120000) { // 45 years, approximately the epoch
    return "more than year ago";
  }

  // TODO pass in Date.now() and ms to check for 0 as never
  return "never";
}

And it's actually exactly 50 lines if we wrap it in simple browser + commonjs style exports:

;(function (exports) {
'use strict';

// ...

exports = exports.timeago = timeago;
}('undefined' !== typeof module ? module.exports : window));

Now, if you looked closely, you noticed that some of the measurements are very exact, but others are less exact, or otherwise unevenly distanced.

For example: 60 seconds is one minute - we all know this - but a "moment" (a unit of measure I made up) is 2 seconds. A user will see "3 weeks" for a full 7 days, but "4 weeks" only lasts for 2 days before becoming "a month". Months are about 30 days and a year is 365 days is a year (even though it isn't always).

Perhaps the inconsistencies bug you and you want to ditch "moments" and "never", or insert "fortnights" and "scores". Rather than being contrained to a full-blown framework of a library, this gives you a "scaffold" to build from.

Good luck!

:)

If you liked this, you may also enjoy Chris Ferdinandi's VanillaJS newsletter. Check it out at https://gomakethings.com/.

But Wait! There's more: i18n

One of the big advantages to the timeago framework is its internationalization. Here's a second version of my scaffold, modified to make i18n easier:

;(function (exports) {
'use strict';

exports.timeago = function timeago(ms, locale) {
  var ago = Math.floor(ms / 1000);
  var part = 0;

  if (ago < MOMENTS) { return locale.moment; }
  if (ago < SECONDS) { return locale.moments; }
  if (ago < MINUTE) { return locale.seconds.replace(/%\w?/, ago); }

  if (ago < (2 * MINUTE)) { return locale.minute; }
  if (ago < HOUR) {
    while (ago >= MINUTE) { ago -= MINUTE; part += 1; }
    return locale.minutes.replace(/%\w?/, part);
  }

  if (ago < (2 * HOUR)) { return locale.hour; }
  if (ago < DAY) {
    while (ago >= HOUR) { ago -= HOUR; part += 1; }
    return locale.hours.replace(/%\w?/, part);
  }

  if (ago < (2 * DAY)) { return locale.day; }
  if (ago < WEEK) {
    while (ago >= DAY) { ago -= DAY; part += 1; }
    return locale.days.replace(/%\w?/, part);
  }

  if (ago < (2 * WEEK)) { return locale.week; }
  if (ago < MONTH) {
    while (ago >= WEEK) { ago -= WEEK; part += 1; }
    return locale.weeks.replace(/%\w?/, part);
  }

  if (ago < (2 * MONTH)) { return locale.month; }
  if (ago < YEAR) { // 45 years, approximately the epoch
    while (ago >= MONTH) { ago -= MONTH; part += 1; }
    return locale.months.replace(/%\w?/, part);
  }

  if (ago < NEVER) {
    return locale.years;
  }

  return locale.never;
}

var MOMENT = 0;
var MOMENTS = 2;
var SECONDS = 5;
var MINUTE = 60;
var HOUR = 60 * MINUTE;
var DAY = 24 * HOUR;
var WEEK = 7 * DAY;
var MONTH = 30 * DAY;
var YEAR = 365 * DAY;
// workaround for when `ms = Date.now() - 0`
var NEVER = 45 * YEAR;

}('undefined' !== typeof module ? module.exports : window));

Simple Testing

Now curse me to hell if you'd like, but this isn't the type of thing that needs a $50/month subscription to a Continuous Integration and Deployment system to test.

If you find that you're editing it on a regular of enough basis to download 6 gigs of dependencies and 8 tool chains using 3 different languages (ruby, python, and node, of course) then by all means go ahead.

However, a much simpler test will do for all practical purposes:

;(function (exports) {
'use strict';

var en = {
  moment: "a moment ago"
, moments: "moments ago"
, seconds: "%s seconds ago"
, minute: "a minute ago"
, minutes: "%m minutes ago"
, hour: "an hour ago"
, hours: "%h hours ago"
, day: "a day ago"
, days: "%D days ago"
, week: "a week ago"
, weeks: "%w weeks ago"
, month: "a month ago"
, months: "%M months ago"
, years: "more than a year ago"
, never: "never"
};

var timeago = exports.timeago || require('./timeago.js').timeago;

function test() {
  [ [ 1.5 * 1000, "a moment ago" ]
  , [ 4.5 * 1000, "moments ago" ]
  , [ 10  * 1000, "10 seconds ago" ]
  , [ 59  * 1000, "59 seconds ago" ]
  , [ 60  * 1000, "a minute ago" ]
  , [ 61  * 1000, "a minute ago" ]
  , [ 119  * 1000, "a minute ago" ]
  , [ 120  * 1000, "2 minutes ago" ]
  , [ 121 * 1000, "2 minutes ago" ]
  , [ (60 * 60 * 1000) - 1000, "59 minutes ago" ]
  , [ 1 * 60 * 60 * 1000, "an hour ago" ]
  , [ 1.5 * 60 * 60 * 1000, "an hour ago" ]
  , [ 2.5 * 60 * 60 * 1000, "2 hours ago" ]
  , [ 1.5 * 24 * 60 * 60 * 1000, "a day ago" ]
  , [ 2.5 * 24 * 60 * 60 * 1000, "2 days ago" ]
  , [ 7 * 24 * 60 * 60 * 1000, "a week ago" ]
  , [ 14 * 24 * 60 * 60 * 1000, "2 weeks ago" ]
  , [ 27 * 24 * 60 * 60 * 1000, "3 weeks ago" ]
  , [ 28 * 24 * 60 * 60 * 1000, "4 weeks ago" ]
  , [ 29 * 24 * 60 * 60 * 1000, "4 weeks ago" ]
  , [ 1.5 * 30 * 24 * 60 * 60 * 1000, "a month ago" ]
  , [ 2.5 * 30 * 24 * 60 * 60 * 1000, "2 months ago" ]
  , [ (12 * 30 * 24 * 60 * 60 * 1000) + 1000, "12 months ago" ]
  , [ 13 * 30 * 24 * 60 * 60 * 1000, "more than a year ago" ]
  , [ 46 * 12 * 30 * 24 * 60 * 60 * 1000, "never" ]
  ].forEach(function (d) {
    var str = timeago(d[0], en);
    if (str !== d[1]) {
      console.error(d[0], str, '!=', d[1]);
    }
  });
}

test();

}('undefined' !== typeof module ? module.exports : window));

Time Ago + Test for Browser + Node in VanillaJS

And there we are!

In a grand total of less than 180 lines of code you have a basic version of Time Ago, an internationalized version, a locale set, and 100% code coverage.

No need for layers of complexity introduced by compontens, imports, exports, babel, TypeScript, or the like. It's just plain and simple, as it should be. :)

One Last Thing: Time From Now

You may have already guessed, but migrating this entire codebase from being exclusively a "time ago" system to a "from now" system is only a matter of replacing the localization file with different words (and swapping Date.now() - ms with ms - Date.now() in your code).

var en = {
  moment: "a moment from now"
, moments: "moments from now"
, seconds: "%d seconds from now"
, minute: "a minute from now"
, minutes: "%d minutes from now"
, hour: "an hour from now"
, hours: "%d hours from now"
, day: "a day from now"
, days: "%d days from now"
, week: "a week from now"
, weeks: "%d weeks from now"
, month: "a month from now"
, months: "%d months from now"
, years: "more than a year from now"
, never: "forever"
};

By AJ ONeal

If you loved this and want more like it, sign up!


Did I make your day?
Buy me a coffeeBuy me a coffee  

(you can learn about the bigger picture I'm working towards on my patreon page )



Published

2018-11-4



Buy me a coffeeBuy me a coffee


  73% of Goal Reached


Want more like this?