Skip to the code
cf.

Relative time

Write relativeTime(date, now). It describes how long before or after now the moment date is, the way feeds and comments do: 3 minutes ago, or in 2 days.

date and now are each a Date or a timestamp: a number of milliseconds, like the ones Date.now() returns. If either is an invalid date, such as new Date("oops") or NaN, throw a RangeError.

When the two are less than a minute apart, either way, return "just now".

Otherwise, use the largest of these units that fits into the difference at least once: year (365 days), month (30 days), week (7 days), day, hour, and minute. Count whole units and drop the rest: 119 seconds is 1 minute, and 13 days is 1 week.

Write the count, a space, and the unit, adding an s when the count isn't 1. If date is before now, add ago, as in 1 hour ago. If it's after now, put in in front, as in in 2 hours.

Write it without Intl.

const now = new Date("2024-05-01T12:00:00Z");
relativeTime(new Date("2024-05-01T11:57:30Z"), now); // "2 minutes ago"
relativeTime(new Date("2024-05-01T15:00:00Z"), now); // "in 3 hours"
relativeTime(new Date("2024-04-21T12:00:00Z"), now); // "1 week ago"
relativeTime(now.getTime() - 20_000, now);           // "just now"

What the tests check

  1. returns just now when the dates are under a minute apart
    const now = Date.UTC(2024, 4, 1, 12);
    expect(relativeTime(now, now)).toBe("just now");
    expect(relativeTime(now - 59_999, now)).toBe("just now");
    expect(relativeTime(now + 59_999, now)).toBe("just now");
    
  2. counts whole minutes and hours, dropping the rest
    const now = Date.UTC(2024, 4, 1, 12);
    const minute = 60 * 1000;
    const hour = 60 * minute;
    expect(relativeTime(now - minute, now)).toBe("1 minute ago");
    expect(relativeTime(now - 150 * 1000, now)).toBe("2 minutes ago");
    expect(relativeTime(now - (hour - 1), now)).toBe("59 minutes ago");
    expect(relativeTime(now - hour, now)).toBe("1 hour ago");
    expect(relativeTime(now - (24 * hour - 1), now)).toBe("23 hours ago");
    
  3. moves up to days, weeks, months, and years
    const now = Date.UTC(2024, 4, 1, 12);
    const day = 24 * 60 * 60 * 1000;
    expect(relativeTime(now - day, now)).toBe("1 day ago");
    expect(relativeTime(now - (7 * day - 1), now)).toBe("6 days ago");
    expect(relativeTime(now - 7 * day, now)).toBe("1 week ago");
    expect(relativeTime(now - 29 * day, now)).toBe("4 weeks ago");
    expect(relativeTime(now - 30 * day, now)).toBe("1 month ago");
    expect(relativeTime(now - 364 * day, now)).toBe("12 months ago");
    expect(relativeTime(now - 365 * day, now)).toBe("1 year ago");
    expect(relativeTime(now - 1000 * day, now)).toBe("2 years ago");
    
  4. adds an s only when the count isn't 1
    const now = Date.UTC(2024, 4, 1, 12);
    const hour = 60 * 60 * 1000;
    const day = 24 * hour;
    expect(relativeTime(now - 2 * 60 * 1000, now)).toBe("2 minutes ago");
    expect(relativeTime(now - hour, now)).toBe("1 hour ago");
    expect(relativeTime(now - 5 * hour, now)).toBe("5 hours ago");
    expect(relativeTime(now - 14 * day, now)).toBe("2 weeks ago");
    expect(relativeTime(now - 45 * day, now)).toBe("1 month ago");
    expect(relativeTime(now - 90 * day, now)).toBe("3 months ago");
    expect(relativeTime(now - 3650 * day, now)).toBe("10 years ago");
    
  5. says in … for dates after now
    const now = Date.UTC(2024, 4, 1, 12);
    const hour = 60 * 60 * 1000;
    const day = 24 * hour;
    expect(relativeTime(now + 3 * hour, now)).toBe("in 3 hours");
    expect(relativeTime(now + day, now)).toBe("in 1 day");
    expect(relativeTime(now + 14 * day, now)).toBe("in 2 weeks");
    expect(relativeTime(now + 400 * day, now)).toBe("in 1 year");
    
  6. drops the rest for dates after now too
    const now = Date.UTC(2024, 4, 1, 12);
    const day = 24 * 60 * 60 * 1000;
    expect(relativeTime(now + 90 * 1000, now)).toBe("in 1 minute");
    expect(relativeTime(now + (2 * day - 1), now)).toBe("in 1 day");
    expect(relativeTime(now + 13 * day, now)).toBe("in 1 week");
    
  7. accepts Date objects and timestamps
    const now = new Date("2024-05-01T12:00:00Z");
    expect(relativeTime(new Date("2024-05-01T11:57:30Z"), now)).toBe("2 minutes ago");
    expect(relativeTime(new Date("2024-05-01T15:00:00Z"), now.getTime())).toBe("in 3 hours");
    expect(relativeTime(Date.UTC(2024, 3, 21, 12), now)).toBe("1 week ago");
    
  8. throws a RangeError for an invalid date
    const now = Date.UTC(2024, 4, 1, 12);
    expect(() => relativeTime(new Date("not a date"), now)).toThrow(RangeError);
    expect(() => relativeTime(now, new Date(NaN))).toThrow(RangeError);
    expect(() => relativeTime(NaN, now)).toThrow(RangeError);
    
  9. works without Intl
    const now = Date.UTC(2024, 4, 1, 12);
    const { RelativeTimeFormat, PluralRules } = Intl;
    Intl.RelativeTimeFormat = function () {
      throw new Error("relativeTime must not use Intl.RelativeTimeFormat");
    };
    Intl.PluralRules = function () {
      throw new Error("relativeTime must not use Intl.PluralRules");
    };
    let results;
    try {
      results = [relativeTime(now - 3 * 60 * 1000, now), relativeTime(now + 2 * 24 * 60 * 60 * 1000, now)];
    } finally {
      // Put the built-ins back even when relativeTime throws, before expect runs.
      Intl.RelativeTimeFormat = RelativeTimeFormat;
      Intl.PluralRules = PluralRules;
    }
    expect(results).toEqual(["3 minutes ago", "in 2 days"]);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).