← back to Govarbitrage
apps/mobile/lib/format.ts
126 lines
/**
* Formatting utilities for financial data, scores, dates, and countdowns.
*/
// ── Currency ──────────────────────────────────────────────────────────────────
export function fmtUSD(value: number | null | undefined, decimals = 0): string {
if (value == null || !Number.isFinite(value)) return "—";
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(value);
}
// Percentages above this ratio are treated as implausible/garbage and rendered
// as "—" instead of an absurd number (e.g. a backend annualizedReturn of
// 23002764 would otherwise display as 2,300,276,400.0%). 1000 ratio = 100,000%,
// generous enough to keep legitimate aggressive returns (e.g. roi 6.83 = 683%).
const MAX_PCT_RATIO = 1000;
export function fmtPct(
value: number | null | undefined,
opts?: { maxRatioAbs?: number; decimals?: number }
): string {
if (value == null || !Number.isFinite(value)) return "—";
// value is a decimal ratio (0.42 = 42%)
const maxAbs = opts?.maxRatioAbs ?? MAX_PCT_RATIO;
if (Math.abs(value) > maxAbs) return "—";
const decimals = opts?.decimals ?? 1;
return `${(value * 100).toFixed(decimals)}%`;
}
// ── Scores ────────────────────────────────────────────────────────────────────
export function fmtScore(score: number | null | undefined): string {
if (score == null || !Number.isFinite(score)) return "—";
return Math.round(score).toString();
}
// ── Time / countdown ──────────────────────────────────────────────────────────
/**
* Returns a human-readable countdown string like "2d 4h" or "45m" or "CLOSED".
* closingAt is an ISO8601 string.
*/
export function closingCountdown(closingAt: string | null | undefined): string {
if (!closingAt) return "—";
const ms = new Date(closingAt).getTime() - Date.now();
if (ms <= 0) return "CLOSED";
const totalMin = Math.floor(ms / 60_000);
const days = Math.floor(totalMin / 1440);
const hours = Math.floor((totalMin % 1440) / 60);
const mins = totalMin % 60;
if (days > 0) return `${days}d ${hours}h`;
if (hours > 0) return `${hours}h ${mins}m`;
return `${mins}m`;
}
export function isClosingSoon(closingAt: string | null | undefined): boolean {
if (!closingAt) return false;
const ms = new Date(closingAt).getTime() - Date.now();
return ms > 0 && ms < 24 * 3_600_000; // within 24h
}
/**
* Formats a date+time for admin cards in local timezone.
* Steve's hard rule: admin cards must show created date AND time.
*/
export function fmtDateTime(iso: string | null | undefined): string {
if (!iso) return "—";
return new Date(iso).toLocaleString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
}
// ── Condition / risk labels ───────────────────────────────────────────────────
export function conditionLabel(c: string | null): string {
const map: Record<string, string> = {
NEW: "New",
LIKE_NEW: "Like New",
USED_GOOD: "Used — Good",
USED_FAIR: "Used — Fair",
FOR_PARTS: "Parts Only",
UNKNOWN: "Unknown",
};
return c ? (map[c] ?? c) : "Unknown";
}
export function sourceLabel(s: string): string {
const map: Record<string, string> = {
GOVDEALS: "GovDeals",
GSA_AUCTIONS: "GSA Auctions",
PUBLIC_SURPLUS: "Public Surplus",
COUNTY: "County",
STATE_SURPLUS: "State Surplus",
UNIVERSITY_SURPLUS: "University Surplus",
MUNICIBID: "Municibid",
BID4ASSETS: "Bid4Assets",
GOINDUSTRY: "GoIndustry",
NETWORK_INTL: "Network Intl",
GRAYS_AU: "Grays",
GOVPLANET: "GovPlanet",
CSV: "CSV Import",
EXTENSION: "Extension",
OTHER: "Other",
};
return map[s] ?? s;
}
export function dropShipLabel(d: string): string {
const map: Record<string, string> = {
EASY: "Drop Ship: Easy",
MODERATE: "Drop Ship: OK",
DIFFICULT: "Drop Ship: Hard",
INFEASIBLE: "No Drop Ship",
};
return map[d] ?? d;
}