mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 20:12:48 +05:30
feat: Design and implement time naturalization helper function
This commit is contained in:
@@ -17,3 +17,90 @@ export class WorldClock {
|
||||
return new WorldClock(new Date(iso));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Naturalizes a standard datetime format into a psychologically realistic subjective duration.
|
||||
* Resolves quantized time formats into natural language phrases based on the relationship
|
||||
* between a reference/current time and a past event time.
|
||||
*/
|
||||
export function naturalizeTime(now: Date, past: Date): string {
|
||||
const deltaMs = now.getTime() - past.getTime();
|
||||
|
||||
// Guard against future dates
|
||||
if (deltaMs <= 0) {
|
||||
return "just now";
|
||||
}
|
||||
|
||||
const deltaSeconds = Math.floor(deltaMs / 1000);
|
||||
const deltaMinutes = Math.floor(deltaSeconds / 60);
|
||||
const deltaHours = Math.floor(deltaMinutes / 60);
|
||||
const deltaDays = Math.floor(deltaHours / 24);
|
||||
|
||||
// --- Tier 1: Pure Relative (delta < 6 hours) ---
|
||||
if (deltaHours < 6) {
|
||||
if (deltaMinutes < 1) return "just now";
|
||||
if (deltaMinutes < 3) return "moments ago";
|
||||
if (deltaMinutes < 10) return "a few minutes ago";
|
||||
if (deltaMinutes < 30) return "several minutes ago";
|
||||
if (deltaMinutes < 45) return "about half an hour ago";
|
||||
if (deltaHours < 1.5) return "about an hour ago";
|
||||
if (deltaHours < 3) return "a couple hours ago";
|
||||
return "a few hours ago";
|
||||
}
|
||||
|
||||
// --- Tier 3: Coarse (delta >= 48 hours / 2 days) ---
|
||||
if (deltaDays >= 2) {
|
||||
if (deltaDays < 3) return "a couple days ago";
|
||||
if (deltaDays < 7) return "a few days ago";
|
||||
if (deltaDays < 14) return "about a week ago";
|
||||
if (deltaDays < 21) return "a couple weeks ago";
|
||||
if (deltaDays < 30) return "a few weeks ago";
|
||||
if (deltaDays < 60) return "about a month ago";
|
||||
if (deltaDays < 90) return "a couple months ago";
|
||||
if (deltaDays < 180) return "a few months ago";
|
||||
if (deltaDays < 365) return "many months ago";
|
||||
if (deltaDays < 730) return "about a year ago";
|
||||
return "years ago";
|
||||
}
|
||||
|
||||
// --- Tier 2: Period-Anchored (6 hours <= delta < 48 hours) ---
|
||||
const pastHour = past.getHours();
|
||||
|
||||
let period: string;
|
||||
if (pastHour >= 5 && pastHour < 12) {
|
||||
period = "morning";
|
||||
} else if (pastHour >= 12 && pastHour < 17) {
|
||||
period = "afternoon";
|
||||
} else if (pastHour >= 17 && pastHour < 21) {
|
||||
period = "evening";
|
||||
} else if (pastHour >= 21 && pastHour < 24) {
|
||||
period = "night";
|
||||
} else if (pastHour >= 0 && pastHour < 3) {
|
||||
period = "midnight";
|
||||
} else {
|
||||
period = "late night";
|
||||
}
|
||||
|
||||
// Subjective day test: waking hours (05:00 - 20:59)
|
||||
const nowHour = now.getHours();
|
||||
const pastIsWaking = pastHour >= 5 && pastHour < 22;
|
||||
const nowIsWaking = nowHour >= 5 && nowHour < 22;
|
||||
|
||||
const isSameSubjectiveDay = pastIsWaking && nowIsWaking && deltaHours < 18;
|
||||
|
||||
if (isSameSubjectiveDay) {
|
||||
return `earlier today, in the ${period}`;
|
||||
}
|
||||
|
||||
if (period === "night") {
|
||||
return "last night";
|
||||
}
|
||||
if (period === "midnight") {
|
||||
return "around midnight";
|
||||
}
|
||||
if (period === "late night") {
|
||||
return "late last night";
|
||||
}
|
||||
|
||||
return `yesterday ${period}`;
|
||||
}
|
||||
|
||||
96
packages/core/tests/clock.test.ts
Normal file
96
packages/core/tests/clock.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { naturalizeTime } from "../src/clock.js";
|
||||
|
||||
describe("TimeNaturalization Unit Tests (Tier 1)", () => {
|
||||
test("future or current times return just now", () => {
|
||||
const now = new Date(2026, 6, 8, 12, 0, 0);
|
||||
const pastFuture = new Date(2026, 6, 8, 12, 0, 5);
|
||||
expect(naturalizeTime(now, pastFuture)).toBe("just now");
|
||||
expect(naturalizeTime(now, now)).toBe("just now");
|
||||
});
|
||||
|
||||
test("Tier 1: relative time ranges (< 6 hours)", () => {
|
||||
const now = new Date(2026, 6, 8, 12, 0, 0);
|
||||
|
||||
// < 1 minute
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 59, 30))).toBe("just now");
|
||||
// < 3 minutes
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 58, 0))).toBe("moments ago");
|
||||
// < 10 minutes
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 52, 0))).toBe("a few minutes ago");
|
||||
// < 30 minutes
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 40, 0))).toBe("several minutes ago");
|
||||
// < 45 minutes
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 20, 0))).toBe("about half an hour ago");
|
||||
// < 1.5 hours (90 minutes)
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 10, 50, 0))).toBe("about an hour ago");
|
||||
// < 3 hours
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 9, 30, 0))).toBe("a couple hours ago");
|
||||
// < 6 hours
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 7, 0, 0))).toBe("a few hours ago");
|
||||
});
|
||||
|
||||
test("Tier 2: Same subjective day vs yesterday periods (6h <= delta < 48h)", () => {
|
||||
// 1. Same subjective day (both waking hours, delta < 18h)
|
||||
// 9am to 3pm (delta 6h) -> morning
|
||||
const nowWaking = new Date(2026, 6, 8, 15, 0, 0);
|
||||
const pastMorning = new Date(2026, 6, 8, 9, 0, 0);
|
||||
expect(naturalizeTime(nowWaking, pastMorning)).toBe("earlier today, in the morning");
|
||||
|
||||
// 2pm to 9pm (delta 7h) -> afternoon
|
||||
const nowEvening = new Date(2026, 6, 8, 21, 0, 0);
|
||||
const pastAfternoon = new Date(2026, 6, 8, 14, 0, 0);
|
||||
expect(naturalizeTime(nowEvening, pastAfternoon)).toBe("earlier today, in the afternoon");
|
||||
|
||||
// 2. Previous night (past period: night (21-23))
|
||||
// 11pm to 5am (delta 6h) -> last night
|
||||
const nowNightRun = new Date(2026, 6, 8, 5, 0, 0);
|
||||
const pastNight = new Date(2026, 6, 7, 23, 0, 0);
|
||||
expect(naturalizeTime(nowNightRun, pastNight)).toBe("last night");
|
||||
|
||||
// 3. Around midnight (past period: midnight (0-2))
|
||||
// 1am to 7am (delta 6h) -> around midnight
|
||||
const nowMidnightRun = new Date(2026, 6, 8, 7, 0, 0);
|
||||
const pastMidnight = new Date(2026, 6, 8, 1, 0, 0);
|
||||
expect(naturalizeTime(nowMidnightRun, pastMidnight)).toBe("around midnight");
|
||||
|
||||
// 4. Late night (past period: late night (3-4))
|
||||
// 3am to 9am (delta 6h) -> late last night
|
||||
const nowLateNightRun = new Date(2026, 6, 8, 9, 0, 0);
|
||||
const pastLateNight = new Date(2026, 6, 8, 3, 0, 0);
|
||||
expect(naturalizeTime(nowLateNightRun, pastLateNight)).toBe("late last night");
|
||||
|
||||
// 5. Yesterday waking hours (delta >= 6h, diff day / waking check fails)
|
||||
// 3pm to 9am next day (delta 18h) -> yesterday afternoon
|
||||
const nowNextDay = new Date(2026, 6, 9, 9, 0, 0);
|
||||
const pastYesterdayAfternoon = new Date(2026, 6, 8, 15, 0, 0);
|
||||
expect(naturalizeTime(nowNextDay, pastYesterdayAfternoon)).toBe("yesterday afternoon");
|
||||
});
|
||||
|
||||
test("Tier 3: Coarse relative time ranges (delta >= 48 hours)", () => {
|
||||
const now = new Date(2026, 6, 10, 12, 0, 0);
|
||||
|
||||
// 2 days
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 0, 0))).toBe("a couple days ago");
|
||||
// 3-6 days
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 6, 12, 0, 0))).toBe("a few days ago");
|
||||
// 7-13 days
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 1, 12, 0, 0))).toBe("about a week ago");
|
||||
// 14-20 days
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 25, 12, 0, 0))).toBe("a couple weeks ago");
|
||||
// 21-29 days
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 15, 12, 0, 0))).toBe("a few weeks ago");
|
||||
// 30-59 days
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 1, 12, 0, 0))).toBe("about a month ago");
|
||||
// 60-89 days
|
||||
expect(naturalizeTime(now, new Date(2026, 4, 1, 12, 0, 0))).toBe("a couple months ago");
|
||||
// 90-179 days
|
||||
expect(naturalizeTime(now, new Date(2026, 3, 1, 12, 0, 0))).toBe("a few months ago");
|
||||
// 180-364 days
|
||||
expect(naturalizeTime(now, new Date(2026, 0, 1, 12, 0, 0))).toBe("many months ago");
|
||||
// 365-729 days
|
||||
expect(naturalizeTime(now, new Date(2025, 6, 1, 12, 0, 0))).toBe("about a year ago");
|
||||
// >= 730 days
|
||||
expect(naturalizeTime(now, new Date(2023, 6, 1, 12, 0, 0))).toBe("years ago");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user