SPB Git forge

spb/vrai-prix

Public

Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.

60commits 1branches 0releases
12.3 MBsize
maindefault branch
17 days agolast push
TypeScript 90.2% JavaScript 3.5% Python 3.4% CSS 1.9% HTML 0.6%
3.5 KB · 80 lines typescript
Raw Blame History
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