javascript
33 lines · 6 steps
Building a human-friendly timeAgo formatter
A relative-time helper walks a table of unit divisors to pick the largest fitting unit and format it with Intl.
Explained by
highlit
1const DIVISIONS = [
2 { amount: 60, unit: 'seconds' },
3 { amount: 60, unit: 'minutes' },
4 { amount: 24, unit: 'hours' },
5 { amount: 7, unit: 'days' },
6 { amount: 4.34524, unit: 'weeks' },
7 { amount: 12, unit: 'months' },
8 { amount: Number.POSITIVE_INFINITY, unit: 'years' },
9];
10
11export function timeAgo(input, locale = 'en') {
12 const date = input instanceof Date ? input : new Date(input);
13
14 if (Number.isNaN(date.getTime())) {
15 throw new TypeError('timeAgo expects a valid date');
16 }
17
18 const formatter = new Intl.RelativeTimeFormat(locale, {
19 numeric: 'auto',
20 style: 'long',
21 });
22
23 let duration = (date.getTime() - Date.now()) / 1000;
24
25 for (const { amount, unit } of DIVISIONS) {
26 if (Math.abs(duration) < amount) {
27 return formatter.format(Math.round(duration), unit);
28 }
29 duration /= amount;
30 }
31
32 return formatter.format(Math.round(duration), 'years');
33}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A ladder of divisors lets you promote a raw second count into the largest natural unit without a chain of if-statements.
- 2Intl.RelativeTimeFormat handles pluralization, sign, and words like 'yesterday' so you never hand-build strings.
- 3Keeping the sign on duration means both past and future dates format correctly from the same loop.
Related explainers
javascript
import { NextResponse } from 'next/server'; import { Redis } from '@upstash/redis'; const redis = Redis.fromEnv();
Sliding-window rate limiting in a Next.js route
rate-limiting
redis
sorted-set
Advanced
8 steps
javascript
const MAX_FILE_SIZE = 5 * 1024 * 1024; const ALLOWED_TYPES = { 'image/jpeg': ['jpg', 'jpeg'],
Validating file uploads by content, not just claims
input-validation
security
magic-bytes
Intermediate
7 steps
javascript
function zip(keys, values) { if (keys.length !== values.length) { throw new RangeError('zip expects arrays of equal length'); }
Three ways to zip arrays in JavaScript
arrays
higher-order-functions
pairing
Intermediate
6 steps
javascript
import { useCallback, useEffect, useState } from 'react'; export function useLocalStorage(key, initialValue) { const readValue = useCallback(() => {
How a useLocalStorage hook syncs state in React
custom hooks
localstorage
state persistence
Intermediate
8 steps
javascript
import { useState } from 'react'; export function ReorderableList({ initialItems }) { const [items, setItems] = useState(initialItems);
Drag-to-reorder lists in React
drag-and-drop
state-management
immutable-updates
Intermediate
8 steps
javascript
import { unstable_cache, revalidateTag } from 'next/cache' import { db } from '@/lib/db' export const getDashboardStats = unstable_cache(
Caching dashboard stats in Next.js
caching
cache-invalidation
tag-based-revalidation
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-human-friendly-timeago-formatter-explained-javascript-68cc/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.