Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.
TypeScript 90.2%
JavaScript 3.5%
Python 3.4%
CSS 1.9%
HTML 0.6%
1// Vrai-Prix — aides de format pour l'onglet « À vendre » (client + serveur).2export const SQFT_M2 = 0.092903;34export const money = (v: number | null | undefined, lang = "fr") =>5 v == null || !Number.isFinite(v)6 ? "—"7 : new Intl.NumberFormat(lang === "fr" ? "fr-CA" : "en-CA", {8 style: "currency",9 currency: "CAD",10 maximumFractionDigits: 0,11 }).format(v);1213export const signedMoney = (v: number, lang = "fr") =>14 `${v >= 0 ? "+" : "−"}${money(Math.abs(Math.round(v)), lang)}`;1516export const num = (v: number | null | undefined, lang = "fr", maxFrac = 0) =>17 v == null || !Number.isFinite(v)18 ? "—"19 : v.toLocaleString(lang === "fr" ? "fr-CA" : "en-CA", { maximumFractionDigits: maxFrac });2021export const pct = (v: number | null | undefined, lang = "fr", frac = 0, signed = true) => {22 if (v == null || !Number.isFinite(v)) return "—";23 const s = Math.abs(v).toLocaleString(lang === "fr" ? "fr-CA" : "en-CA", { maximumFractionDigits: frac });24 return `${signed ? (v >= 0 ? "+" : "−") : ""}${s} %`;25};2627export const dateFr = (iso: string | null | undefined, lang = "fr") => {28 if (!iso) return "—";29 const d = new Date(iso.length <= 10 ? `${iso}T12:00:00` : iso);30 if (Number.isNaN(d.getTime())) return iso;31 return d.toLocaleDateString(lang === "fr" ? "fr-CA" : "en-CA", { year: "numeric", month: "short", day: "numeric" });32};3334export const epochDate = (ts: number | null | undefined, lang = "fr") =>35 ts ? dateFr(new Date(ts * 1000).toISOString(), lang) : "—";3637/** pi² → m² arrondi (superficie habitable plausible seulement). */38export const sqftToM2 = (sqft: number | null | undefined): number | null =>39 sqft && sqft > 50 ? Math.round(sqft * SQFT_M2) : null;4041export const m2 = (v: number | null | undefined, lang = "fr") => (v == null ? "—" : `${num(v, lang)} m²`);4243/** Ratio estimation / prix demandé → libellé de lecture. */44export function ratioLabel(ratio: number | null | undefined, fr: boolean): { text: string; tone: "up" | "down" | "flat" | "none" } {45 if (ratio == null) return { text: fr ? "Non évaluée" : "Not evaluated", tone: "none" };46 const d = (ratio - 1) * 100;47 if (Math.abs(d) < 5) return { text: fr ? "Prix aligné sur l'estimation" : "Price in line with estimate", tone: "flat" };48 if (d > 0) return { text: fr ? `Estimation ${pct(d, fr ? "fr" : "en")} vs prix` : `Estimate ${pct(d, "en")} vs price`, tone: "up" };49 return { text: fr ? `Estimation ${pct(d, "fr")} vs prix` : `Estimate ${pct(d, "en")} vs price`, tone: "down" };50}5152export const median = (xs: number[]): number | null => {53 if (!xs.length) return null;54 const s = [...xs].sort((a, b) => a - b);55 const m = Math.floor(s.length / 2);56 return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;57};5859export const mean = (xs: number[]): number | null => (xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : null);6061export function weightedMean(values: number[], weights: number[]): number | null {62 const tw = weights.reduce((a, b) => a + b, 0);63 if (!tw) return null;64 return values.reduce((s, v, i) => s + v * weights[i], 0) / tw;65}6667export function weightedMedian(values: number[], weights: number[]): number | null {68 const order = values.map((v, i) => ({ v, w: weights[i] })).sort((a, b) => a.v - b.v);69 const total = order.reduce((s, o) => s + o.w, 0);70 if (total <= 0) return null;71 let acc = 0;72 for (const o of order) {73 acc += o.w;74 if (acc >= total / 2) return o.v;75 }76 return order[order.length - 1]?.v ?? null;77}7879export const round100 = (v: number) => Math.round(v / 100) * 100;80