Skip to content

Commit 39f424e

Browse files
committed
fix: show one year instead of 12 months in relative timestamps
Listings updated 360-364 days ago rendered as "12mo ago" because 30-day months do not tile a 365-day year, leaving a five-day gap that still divided into twelve whole months. Derive years from whole months so they roll over at 12, matching formatRelativeUpdatedAt in routes/user/$handle.tsx, which already caps months at 11.
1 parent aaa7362 commit 39f424e

2 files changed

Lines changed: 70 additions & 7 deletions

File tree

src/lib/timeAgo.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import { timeAgo } from "./timeAgo";
3+
4+
const MINUTE = 60_000;
5+
const HOUR = 60 * MINUTE;
6+
const DAY = 24 * HOUR;
7+
8+
const NOW = Date.UTC(2026, 5, 23, 12, 0, 0);
9+
10+
function agoByDays(days: number) {
11+
return timeAgo(NOW - days * DAY);
12+
}
13+
14+
describe("timeAgo", () => {
15+
beforeEach(() => {
16+
vi.useFakeTimers();
17+
vi.setSystemTime(NOW);
18+
});
19+
20+
afterEach(() => {
21+
vi.useRealTimers();
22+
});
23+
24+
it("reports sub-minute gaps as just now", () => {
25+
expect(timeAgo(NOW)).toBe("just now");
26+
expect(timeAgo(NOW - 59_999)).toBe("just now");
27+
});
28+
29+
it("formats minutes and hours", () => {
30+
expect(timeAgo(NOW - MINUTE)).toBe("1m ago");
31+
expect(timeAgo(NOW - 59 * MINUTE)).toBe("59m ago");
32+
expect(timeAgo(NOW - HOUR)).toBe("1h ago");
33+
expect(timeAgo(NOW - 23 * HOUR)).toBe("23h ago");
34+
});
35+
36+
it("formats days and weeks", () => {
37+
expect(agoByDays(1)).toBe("1d ago");
38+
expect(agoByDays(6)).toBe("6d ago");
39+
expect(agoByDays(7)).toBe("1w ago");
40+
expect(agoByDays(29)).toBe("4w ago");
41+
});
42+
43+
it("formats months", () => {
44+
expect(agoByDays(30)).toBe("1mo ago");
45+
expect(agoByDays(60)).toBe("2mo ago");
46+
expect(agoByDays(180)).toBe("6mo ago");
47+
});
48+
49+
// Regression: 30-day months divided into a 365-day year yielded "12mo ago"
50+
// for gaps of 360-364 days. Months must roll over into years at 12.
51+
it("rolls the last days before a year over into years", () => {
52+
expect(agoByDays(330)).toBe("11mo ago");
53+
expect(agoByDays(359)).toBe("11mo ago");
54+
expect(agoByDays(360)).toBe("1y ago");
55+
expect(agoByDays(364)).toBe("1y ago");
56+
});
57+
58+
it("formats years", () => {
59+
expect(agoByDays(365)).toBe("1y ago");
60+
expect(agoByDays(400)).toBe("1y ago");
61+
expect(agoByDays(730)).toBe("2y ago");
62+
});
63+
});

src/lib/timeAgo.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ const HOUR = 60 * MINUTE;
33
const DAY = 24 * HOUR;
44
const WEEK = 7 * DAY;
55
const MONTH = 30 * DAY;
6-
const YEAR = 365 * DAY;
76

87
export function timeAgo(timestamp: number): string {
98
const diff = Date.now() - timestamp;
@@ -24,10 +23,11 @@ export function timeAgo(timestamp: number): string {
2423
const w = Math.floor(diff / WEEK);
2524
return `${w}w ago`;
2625
}
27-
if (diff < YEAR) {
28-
const m = Math.floor(diff / MONTH);
29-
return `${m}mo ago`;
30-
}
31-
const y = Math.floor(diff / YEAR);
32-
return `${y}y ago`;
26+
// Derive years from whole 30-day months rather than from a separate 365-day
27+
// year: dividing a 365-day year by 30-day months leaves a 360-364 day gap
28+
// that rendered as "12mo ago". Matches formatRelativeUpdatedAt in
29+
// routes/user/$handle.tsx.
30+
const months = Math.floor(diff / MONTH);
31+
if (months < 12) return `${months}mo ago`;
32+
return `${Math.floor(months / 12)}y ago`;
3333
}

0 commit comments

Comments
 (0)