/** * A minimal RFC 4180 reader, for the `/csv` batch endpoint. * * Values may contain commas and are then double-quoted with `""` escaping, so * splitting on `,` corrupts every row that carries a street name like * `"Rua Um, Dois"`. This is here because the platform ships no CSV parser and * the library ships no dependencies. * * @module */ /** One parsed CSV record, keyed by the header row. */ export type CsvRecord = Record; function parseRows(text: string): string[][] { const rows: string[][] = []; let row: string[] = []; let field = ""; let quoted = false; let started = false; for (let i = 0; i < text.length; i += 1) { const char = text[i]; if (quoted) { if (char === '"') { if (text[i + 1] === '"') { field += '"'; i += 1; } else { quoted = false; } } else { field += char; } continue; } if (char === '"') { quoted = true; started = true; } else if (char === ",") { row.push(field); field = ""; started = true; } else if (char === "\n") { row.push(field); rows.push(row); row = []; field = ""; started = false; } else if (char !== "\r") { field += char; started = true; } } if (started || field !== "" || row.length > 0) { row.push(field); rows.push(row); } return rows; } /** * Parse CSV text into records keyed by its header row. * * A leading byte-order mark is dropped, `CRLF` and `LF` are both accepted, and * a row shorter than the header has its remaining columns filled with the * empty string rather than `undefined`. An empty input yields no records. */ export function parseCsv(text: string): CsvRecord[] { const rows = parseRows(text.replace(/^\uFEFF/, "")); const header = rows.shift(); if (header === undefined) { return []; } const records: CsvRecord[] = []; for (const row of rows) { if (row.length === 1 && row[0] === "") { continue; } const record: CsvRecord = {}; for (let i = 0; i < header.length; i += 1) { record[header[i] as string] = row[i] ?? ""; } records.push(record); } return records; }