Reltime
Turn a timestamp into a short human 'when' (3h ago, Jun 20) where the whole point is what it won't do: a missing, empty, or unparseable stamp returns no label rather than a guess, a future stamp returns no label rather than a negative age, and anything older than a week gets the real date it landed instead of a rounded-up '9d ago'. Deterministic — a pure function of (stamp, now).
The honest edge
It renders in UTC and is a recency label, not a locale-aware or timezone-shifting formatter, and not a full date library. The fixed minute/hour/day/week bands are by design — the value is the refusal to fabricate, not configurable granularity.
Run it
node reltime.js 2026-08-20T09:00:00Z
test_reltime.js (20/20, mutation-bitten)
Node, no dependencies
The code — every file that ships
reltime.js106 lineson GitHub →
#!/usr/bin/env node
/* reltime.js — an honest relative-time label ("3h ago", "Jun 20") that refuses
to fake, round up, or invent a time it doesn't have.
Turn a timestamp into a short human "when" — but the whole point is what it
WON'T do:
• FLAG, DON'T FAKE. No stamp, an empty stamp, or an unparseable value
returns null — NO label. The caller shows nothing rather than a guessed
or fabricated time. An undated thing is never handed a "when".
• A FUTURE STAMP IS NOT A RECENCY CLAIM. If the timestamp is ahead of now
(clock skew, a bad record), it returns null rather than "-2h ago". You
cannot honestly say how long ago something happened if it hasn't.
• REAL, NOT ROUNDED-UP. Recent items get a relative label
(just now / Nm / Nh / Nd ago); anything older than a week gets the
ABSOLUTE short date it actually landed ("Jun 20", or "Jun 20, 2025"
across a year boundary) — because "9d ago" is less honest and less
useful than the date itself.
• DETERMINISTIC. `now` is injected, so the label is a pure function of
(stamp, now) — testable with no wall clock and stable across a render.
Pure function of its inputs, no dependencies. The same code runs in a browser
(attach relativeTime to your namespace) or on Node (this CLI / require()).
USAGE
node reltime.js 2026-08-20T09:00:00Z # label vs. now
node reltime.js 2026-08-20T09:00:00Z 1755772800000 # label vs. an injected now (ms)
node reltime.js --help
Dates are read/rendered in UTC for determinism. Released under MIT.
*/
"use strict";
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var MIN = 60 * 1000, HOUR = 60 * MIN, DAY = 24 * HOUR, WEEK = 7 * DAY;
/* relativeTime(iso, nowMs) -> short label string, or null when there is no
honest label to show. `iso` is an ISO-8601 string (or null). `nowMs` defaults
to Date.now() but is injected in tests and via the CLI for determinism. */
function relativeTime(iso, nowMs) {
// flag-don't-fake: no stamp, empty stamp, or a non-string -> no label.
if (iso == null) return null;
var s = String(iso);
if (s === "") return null;
var t = Date.parse(s);
if (isNaN(t)) return null; // unparseable -> no fabricated time
var now = (typeof nowMs === "number" && isFinite(nowMs)) ? nowMs : Date.now();
var delta = now - t;
// A future stamp (clock skew) is not a recency claim we can make honestly.
if (delta < 0) return null;
if (delta < MIN) return "just now";
if (delta < HOUR) return Math.floor(delta / MIN) + "m ago";
if (delta < DAY) return Math.floor(delta / HOUR) + "h ago";
if (delta < WEEK) return Math.floor(delta / DAY) + "d ago";
// Older than a week -> the absolute short date it actually landed.
var d = new Date(t);
var label = MONTHS[d.getUTCMonth()] + " " + d.getUTCDate();
if (d.getUTCFullYear() !== new Date(now).getUTCFullYear()) {
label += ", " + d.getUTCFullYear();
}
return label;
}
// Browser: attach to a namespace. Node/require: export. CLI: run below.
if (typeof window !== "undefined") {
window.ForestGifts = window.ForestGifts || {};
window.ForestGifts.relativeTime = relativeTime;
}
if (typeof module !== "undefined" && module.exports) {
module.exports = { relativeTime: relativeTime };
}
// ---- CLI (runs only when invoked directly, never on require) ----------------
function main(argv) {
var args = argv.slice(2);
if (args.length === 0 || args.indexOf("--help") !== -1 || args.indexOf("-h") !== -1) {
process.stdout.write(
"reltime.js — an honest relative-time label that refuses to fake a time.\n\n" +
" node reltime.js <iso-timestamp> label vs. now\n" +
" node reltime.js <iso-timestamp> <now-ms> label vs. an injected now (ms)\n" +
" node reltime.js --help\n\n" +
"Prints the label, or '(no honest label)' when the stamp is missing,\n" +
"unparseable, or in the future. Dates render in UTC.\n"
);
return args.length === 0 ? 1 : 0;
}
var iso = args[0];
var nowMs = args.length > 1 ? Number(args[1]) : undefined;
var label = relativeTime(iso, nowMs);
process.stdout.write((label === null ? "(no honest label)" : label) + "\n");
return 0;
}
if (typeof require !== "undefined" && require.main === module) {
process.exitCode = main(process.argv);
}
test_reltime.js61 lineson GitHub →
#!/usr/bin/env node
/* test_reltime.js — proves the label is honest, exact, and deterministic.
The honesty rules ARE the tool, so the test's real job is to prove each
REFUSAL fires (null, not a fabricated label) and each threshold is exact. The
clock is injected, so every case is deterministic with no wall clock. A
mutation bite guards against a vacuously-green run. Exit 0 = all pass, exit 1
= a failure (loud). stdlib only, no dependencies. */
"use strict";
var relativeTime = require("./reltime.js").relativeTime;
var pass = 0, fail = 0;
function eq(name, got, want) {
var g = JSON.stringify(got), w = JSON.stringify(want);
if (g === w) { pass++; }
else { fail++; console.error("FAIL " + name + "\n got: " + g + "\n want: " + w); }
}
// A fixed "now" so every case is deterministic. 2026-06-27T12:00:00Z.
var NOW = Date.parse("2026-06-27T12:00:00Z");
function ago(ms) { return new Date(NOW - ms).toISOString(); }
var MIN = 60000, HOUR = 60 * MIN, DAY = 24 * HOUR, WEEK = 7 * DAY;
// --- flag-don't-fake: every "no honest label" case returns null -------------
eq("null stamp -> null", relativeTime(null, NOW), null);
eq("undefined stamp -> null", relativeTime(undefined, NOW), null);
eq("empty string -> null", relativeTime("", NOW), null);
eq("unparseable -> null", relativeTime("not a date", NOW), null);
eq("garbage -> null", relativeTime("2026-13-99", NOW), null);
eq("future stamp -> null", relativeTime(ago(-HOUR), NOW), null); // 1h in the FUTURE
// --- relative band, exact thresholds ---------------------------------------
eq("0ms -> just now", relativeTime(ago(0), NOW), "just now");
eq("59s -> just now", relativeTime(ago(59 * 1000), NOW), "just now");
eq("60s -> 1m ago", relativeTime(ago(MIN), NOW), "1m ago");
eq("59m -> 59m ago", relativeTime(ago(59 * MIN), NOW), "59m ago");
eq("60m -> 1h ago", relativeTime(ago(HOUR), NOW), "1h ago");
eq("23h -> 23h ago", relativeTime(ago(23 * HOUR), NOW), "23h ago");
eq("24h -> 1d ago", relativeTime(ago(DAY), NOW), "1d ago");
eq("6d -> 6d ago", relativeTime(ago(6 * DAY), NOW), "6d ago");
// --- absolute band (>= 1 week): the real date it landed --------------------
eq("7d -> absolute date", relativeTime(ago(WEEK), NOW), "Jun 20");
eq("30d same year -> date", relativeTime(ago(30 * DAY), NOW), "May 28");
eq("across a year boundary", relativeTime("2025-12-31T00:00:00Z", NOW), "Dec 31, 2025");
// --- determinism: same inputs, same output ---------------------------------
eq("deterministic", relativeTime(ago(3 * HOUR), NOW), relativeTime(ago(3 * HOUR), NOW));
// --- coercion / robustness --------------------------------------------------
eq("non-string stamp -> null", relativeTime(12345, NOW), null); // a number is not an ISO string here
// --- mutation bite: the future-guard MUST be load-bearing ------------------
// If someone deleted `if (delta < 0) return null;`, a future stamp would
// produce "just now" (delta<MIN with negative delta). Assert it does NOT.
var futureResult = relativeTime(ago(-HOUR), NOW);
if (futureResult === null) { pass++; }
else { fail++; console.error("FAIL mutation-bite: future stamp produced a label: " + JSON.stringify(futureResult)); }
console.log("\nreltime: " + pass + " passed, " + fail + " failed");
process.exit(fail === 0 ? 0 : 1);