// Vrai-Prix — aides de format pour l'onglet « À vendre » (client + serveur). export const SQFT_M2 = 0.092903; export const money = (v: number | null | undefined, lang = "fr") => v == null || !Number.isFinite(v) ? "—" : new Intl.NumberFormat(lang === "fr" ? "fr-CA" : "en-CA", { style: "currency", currency: "CAD", maximumFractionDigits: 0, }).format(v); export const signedMoney = (v: number, lang = "fr") => `${v >= 0 ? "+" : "−"}${money(Math.abs(Math.round(v)), lang)}`; export const num = (v: number | null | undefined, lang = "fr", maxFrac = 0) => v == null || !Number.isFinite(v) ? "—" : v.toLocaleString(lang === "fr" ? "fr-CA" : "en-CA", { maximumFractionDigits: maxFrac }); export const pct = (v: number | null | undefined, lang = "fr", frac = 0, signed = true) => { if (v == null || !Number.isFinite(v)) return "—"; const s = Math.abs(v).toLocaleString(lang === "fr" ? "fr-CA" : "en-CA", { maximumFractionDigits: frac }); return `${signed ? (v >= 0 ? "+" : "−") : ""}${s} %`; }; export const dateFr = (iso: string | null | undefined, lang = "fr") => { if (!iso) return "—"; const d = new Date(iso.length <= 10 ? `${iso}T12:00:00` : iso); if (Number.isNaN(d.getTime())) return iso; return d.toLocaleDateString(lang === "fr" ? "fr-CA" : "en-CA", { year: "numeric", month: "short", day: "numeric" }); }; export const epochDate = (ts: number | null | undefined, lang = "fr") => ts ? dateFr(new Date(ts * 1000).toISOString(), lang) : "—"; /** pi² → m² arrondi (superficie habitable plausible seulement). */ export const sqftToM2 = (sqft: number | null | undefined): number | null => sqft && sqft > 50 ? Math.round(sqft * SQFT_M2) : null; export const m2 = (v: number | null | undefined, lang = "fr") => (v == null ? "—" : `${num(v, lang)} m²`); /** Ratio estimation / prix demandé → libellé de lecture. */ export function ratioLabel(ratio: number | null | undefined, fr: boolean): { text: string; tone: "up" | "down" | "flat" | "none" } { if (ratio == null) return { text: fr ? "Non évaluée" : "Not evaluated", tone: "none" }; const d = (ratio - 1) * 100; if (Math.abs(d) < 5) return { text: fr ? "Prix aligné sur l'estimation" : "Price in line with estimate", tone: "flat" }; if (d > 0) return { text: fr ? `Estimation ${pct(d, fr ? "fr" : "en")} vs prix` : `Estimate ${pct(d, "en")} vs price`, tone: "up" }; return { text: fr ? `Estimation ${pct(d, "fr")} vs prix` : `Estimate ${pct(d, "en")} vs price`, tone: "down" }; } export const median = (xs: number[]): number | null => { if (!xs.length) return null; const s = [...xs].sort((a, b) => a - b); const m = Math.floor(s.length / 2); return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; }; export const mean = (xs: number[]): number | null => (xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : null); export function weightedMean(values: number[], weights: number[]): number | null { const tw = weights.reduce((a, b) => a + b, 0); if (!tw) return null; return values.reduce((s, v, i) => s + v * weights[i], 0) / tw; } export function weightedMedian(values: number[], weights: number[]): number | null { const order = values.map((v, i) => ({ v, w: weights[i] })).sort((a, b) => a.v - b.v); const total = order.reduce((s, o) => s + o.w, 0); if (total <= 0) return null; let acc = 0; for (const o of order) { acc += o.w; if (acc >= total / 2) return o.v; } return order[order.length - 1]?.v ?? null; } export const round100 = (v: number) => Math.round(v / 100) * 100;