/** * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * Project: Groupe Ka / Ka Maps * * Canadian-French price/value formatting shared by all map labels. */ /** * Compact price label for map markers, French-Canadian style. * * 289 000 $ → "289 k$" * 499 900 $ → "500 k$" * 1 250 000 $ → "1,25 M$" * 2 800 000 $ → "2,8 M$" * 950 $ (loyer) → "950 $" */ export function formatCompactPrice(value: number): string { if (!Number.isFinite(value) || value < 0) return ""; if (value < 10_000) { // Rents and small amounts: whole dollars, non-breaking thin space groups. return `${formatGroupedInt(Math.round(value))} $`; } if (value < 1_000_000) { const k = Math.round(value / 1000); if (k >= 1000) return formatMillions(value); // 999 500+ rounds into M$ return `${k} k$`; } return formatMillions(value); } function formatMillions(value: number): string { const m = value / 1_000_000; // Two decimals under 10 M$, one is enough above; strip trailing zeros. const rounded = m < 10 ? Math.round(m * 100) / 100 : Math.round(m * 10) / 10; const text = rounded .toFixed(rounded < 10 ? 2 : 1) .replace(/\.?0+$/, "") .replace(".", ","); return `${text} M$`; } /** Full price, French-Canadian: 589 000 $ (narrow no-break group separators). */ export function formatFullPrice(value: number): string { if (!Number.isFinite(value)) return ""; return `${formatGroupedInt(Math.round(value))} $`; } /** Signed percentage, French style: +5,8 % / -1,2 %. */ export function formatPercent(ratio: number, decimals = 1): string { if (!Number.isFinite(ratio)) return ""; const pct = ratio * 100; const sign = pct > 0 ? "+" : ""; return `${sign}${pct.toFixed(decimals).replace(".", ",")} %`; } /** Group an integer with narrow no-break spaces: 1250000 → "1 250 000". */ function formatGroupedInt(n: number): string { const sign = n < 0 ? "-" : ""; const digits = Math.abs(n).toString(); const grouped = digits.replace(/\B(?=(\d{3})+(?!\d))/g, " "); return sign + grouped; } /** Median of a numeric array (returns undefined for empty input). */ export function median(values: number[]): number | undefined { const clean = values.filter((v) => Number.isFinite(v)).sort((a, b) => a - b); if (clean.length === 0) return undefined; const mid = Math.floor(clean.length / 2); return clean.length % 2 === 1 ? clean[mid] : ((clean[mid - 1] as number) + (clean[mid] as number)) / 2; }