// Small RFC 4180 CSV reader / writer (quotes, doubled quotes, CR LF, a UTF-8 BOM). Pure. Used for tester lists, // price sheets and the Play statistics exports. import { csvCell } from './data-safety'; /** Rows of cells. Empty lines are dropped. */ export function parseCsv(text: string, sep = ','): string[][] { const rows: string[][] = []; let row: string[] = []; let cell = ''; let quoted = false; const s = text.replace(/^/, ''); for (let i = 0; i < s.length; i++) { const ch = s[i]!; if (quoted) { if (ch === '"') { if (s[i + 1] === '"') { cell += '"'; i++; } else quoted = false; } else cell += ch; continue; } if (ch === '"' && cell === '') { quoted = true; continue; } if (ch === sep) { row.push(cell); cell = ''; continue; } if (ch === '\n' || ch === '\r') { if (ch === '\r' && s[i + 1] === '\n') i++; row.push(cell); cell = ''; if (row.some(c => c !== '')) rows.push(row); row = []; continue; } cell += ch; } row.push(cell); if (row.some(c => c !== '')) rows.push(row); return rows; } /** Header row -> objects keyed by the lower-cased, trimmed header (spaces and dashes become "_"). */ export function parseCsvRecords(text: string, sep = ','): Record[] { const [head, ...rest] = parseCsv(text, sep); if (!head) return []; const keys = head.map(h => h.trim().toLowerCase().replace(/[\s-]+/g, '_')); return rest.map(r => Object.fromEntries(keys.map((k, i) => [k, (r[i] ?? '').trim()]))); } export const toCsv = (rows: (string | number)[][]): string => rows.map(r => r.map(c => csvCell(String(c))).join(',')).join('\n') + '\n';