Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125const DIVISIONS: { amount: number; unit: Intl.RelativeTimeFormatUnit }[] = [ { amount: 60, unit: "seconds" }, { amount: 60, unit: "minutes" }, { amount: 24, unit: "hours" }, { amount: 7, unit: "days" }, { amount: 4.34524, unit: "weeks" }, { amount: 12, unit: "months" }, { amount: Number.POSITIVE_INFINITY, unit: "years" }];
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });const dtf = new Intl.DateTimeFormat("en", { year: "numeric", month: "short", day: "numeric" });const dtfMonthDay = new Intl.DateTimeFormat("en", { month: "short", day: "numeric" });const dtfUtc = new Intl.DateTimeFormat("en", { year: "numeric", month: "short", day: "numeric", timeZone: "UTC"});// the appview's longTimeFmt ("Jan 2, 2006, 3:04 PM MST")const dtfFull = new Intl.DateTimeFormat("en", { year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short"});
// "3 days ago", "yesterday", etc.// a future time is clock skew between machines, so it reads as nowexport const relativeTime = (input: string | Date, now: Date = new Date()): string => { const date = typeof input === "string" ? new Date(input) : input; if (Number.isNaN(date.getTime())) return ""; if (date.getTime() === 0) return "at the beginning of time"; let duration = Math.min(date.getTime() - now.getTime(), 0) / 1000; for (const division of DIVISIONS) { if (Math.abs(duration) < division.amount) return rtf.format(Math.round(duration), division.unit); duration /= division.amount; } return rtf.format(Math.round(duration), "years");};export const compactRelativeTime = (input: string | Date, now: Date = new Date()): string => { const date = typeof input === "string" ? new Date(input) : input; if (Number.isNaN(date.getTime())) return ""; let duration = (now.getTime() - date.getTime()) / 1000; const suffix = "ago";
if (duration < 1) return "now"; if (duration < 60) return `${Math.floor(duration)}s ${suffix}`; duration /= 60; if (duration < 60) return `${Math.floor(duration)}m ${suffix}`; duration /= 60; if (duration < 24) return `${Math.floor(duration)}h ${suffix}`; duration /= 24; if (duration < 7) return `${Math.floor(duration)}d ${suffix}`; duration /= 7; if (duration < 4.34524) return `${Math.floor(duration)}w ${suffix}`; duration /= 4.34524; if (duration < 12) return `${Math.floor(duration)}mo ${suffix}`; duration /= 12; return `${Math.floor(duration)}y ${suffix}`;};
export const formatDuration = (ms: number): string => { if (!Number.isFinite(ms) || ms < 0) return ""; const total = Math.floor(ms / 1000); const chunks: [number, string][] = [ [Math.floor(total / 86400), "d"], [Math.floor(total / 3600) % 24, "h"], [Math.floor(total / 60) % 60, "m"], [total % 60, "s"] ]; const parts = chunks .filter(([amount]) => amount !== 0) .map(([amount, unit]) => `${amount}${unit}`); return parts.length > 0 ? parts.join(" ") : "0s";};
export const formatDate = (input: string | Date): string => { const date = typeof input === "string" ? new Date(input) : input; return Number.isNaN(date.getTime()) ? "" : dtf.format(date);};
export const formatShortDate = (input: string | Date, now: Date = new Date()): string => { const date = typeof input === "string" ? new Date(input) : input; if (Number.isNaN(date.getTime())) return ""; return date.getFullYear() === now.getFullYear() ? dtfMonthDay.format(date) : dtf.format(date);};
export const formatCalendarDate = (input: string): string => { const date = new Date(input); return Number.isNaN(date.getTime()) ? "" : dtfUtc.format(date);};
export const formatDateTime = (input: string | Date): string => { const date = typeof input === "string" ? new Date(input) : input; return Number.isNaN(date.getTime()) ? "" : dtfFull.format(date);};
export const formatCount = (n: number | undefined | null): string => { if (n == null) return ""; if (n < 1000) return String(n); const [scaled, suffix] = n < 1_000_000 ? [n / 1000, "k"] : [n / 1_000_000, "m"]; // truncate so 999,999 reads as 999.9k rather than rounding up to 1000k return `${Math.floor(scaled * 10) / 10}${suffix}`;};
const pluralRules = new Intl.PluralRules("en");
// the noun alone, for markup that renders the number separatelyexport const plural = (n: number, one: string, other: string): string => pluralRules.select(n) === "one" ? one : other;
// "1 follower", "1.2k followers"export const pluralize = (n: number, one: string, other: string): string => `${formatCount(n)} ${plural(n, one, other)}`;
export const sentence = (text: string): string => { const trimmed = text.trim(); const capitalized = trimmed.charAt(0).toUpperCase() + trimmed.slice(1); return /[.!?]$/.test(capitalized) ? capitalized : `${capitalized}.`;};