/** * Recency filtering for search results. * * Pure functions (no `pi`/`ctx`) so the windowing/sorting semantics can be * unit-tested. `SearchResult` is defined in `search.ts`. */ import type { SearchResult } from "../search/search.ts"; export type Recency = "day" | "week" | "month" | "year"; export const WINDOW_DAYS: Record = { day: 1, week: 7, month: 31, year: 366 }; export function ageDays(published?: string): number | undefined { if (!published) return undefined; const t = Date.parse(published); if (Number.isNaN(t)) return undefined; return (Date.now() - t) / 86_400_000; } export function ageLabel(days: number): string { const d = Math.round(days); if (d < 0) return "future-dated"; // publisher/data quirk; not actually "today" if (d === 0) return "today"; if (d === 1) return "1 day ago"; if (d < 30) return `${d} days ago`; const m = Math.floor(d / 30); return m === 1 ? "1 month ago" : `${m} months ago`; } export function dateSuffix(r: SearchResult): string { const d = ageDays(r.published); if (r.published && d !== undefined) return ` — ${r.published.slice(0, 10)} (${ageLabel(d)})`; return ""; } /** * When a recency window is set: keep in-window dated results (newest first), * drop clearly-older dated results, and (unless strict) keep undated results as * a fallback tail. Strict mode returns only provably in-window dated results. */ export function applyRecency(results: SearchResult[], recency: Recency, strict: boolean): SearchResult[] { const max = WINDOW_DAYS[recency]; const dated: { r: SearchResult; d: number }[] = []; const undated: SearchResult[] = []; for (const r of results) { const d = ageDays(r.published); if (d === undefined) { if (!strict) undated.push(r); // can't time-bound undated pages } else if (d < -1) { if (!strict) undated.push(r); // future-dated (>1d ahead): suspect, don't rank first } else if (d <= max) { dated.push({ r, d }); // in-window (small future tolerance allowed) } // else: clearly older than the window -> dropped } dated.sort((a, b) => a.d - b.d); // newest first return strict ? dated.map((x) => x.r) : [...dated.map((x) => x.r), ...undated]; }