Repository Public
1// UQO Éval — Métré : analyseur CSV minimal (RFC 4180, guillemets, séparateur , ou ;). Module PUR.2export function parseCsv(text: string, opts: { delimiter?: "," | ";" | "\t"; maxRows?: number } = {}): { header: string[]; rows: string[][] } {3 const src = text.replace(/^/, "");4 const delim = opts.delimiter ?? detectDelimiter(src);5 const rows: string[][] = [];6 let row: string[] = [], field = "", q = false;7 for (let i = 0; i < src.length; i++) {8 const c = src[i];9 if (q) {10 if (c === '"') { if (src[i + 1] === '"') { field += '"'; i++; } else q = false; }11 else field += c;12 } else if (c === '"') q = true;13 else if (c === delim) { row.push(field); field = ""; }14 else if (c === "\n" || c === "\r") { if (c === "\r" && src[i + 1] === "\n") i++; row.push(field); field = ""; if (row.some((x) => x !== "")) rows.push(row); row = []; if (opts.maxRows && rows.length > opts.maxRows) break; }15 else field += c;16 }17 if (field !== "" || row.length) { row.push(field); if (row.some((x) => x !== "")) rows.push(row); }18 const header = (rows.shift() ?? []).map((h) => h.trim());19 return { header, rows };20}2122export function detectDelimiter(text: string): "," | ";" | "\t" {23 const first = text.split(/\r?\n/, 1)[0] ?? "";24 const c = (first.match(/,/g) ?? []).length, s = (first.match(/;/g) ?? []).length, t = (first.match(/\t/g) ?? []).length;25 return t > c && t > s ? "\t" : s > c ? ";" : ",";26}2728/** Nombre depuis une chaîne FR/EN (« 1 234,56 $ », "1,234.56"). */29export function parseNumber(s: string | undefined | null): number | null {30 if (s == null) return null;31 let t = String(s).replace(/[$\s ]/g, "").trim();32 if (!t) return null;33 if (/,\d{1,2}$/.test(t) && !/\.\d/.test(t)) t = t.replace(/\./g, "").replace(",", ".");34 else t = t.replace(/,/g, "");35 const n = Number(t);36 return Number.isFinite(n) ? n : null;37}3839export function normHeader(h: string): string {40 return h.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");41}42