45 lines
1.8 KiB
TypeScript
45 lines
1.8 KiB
TypeScript
// 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.
|
|
|
|
/** One CSV cell, quoted when it holds a quote, comma or newline. */
|
|
export function csvCell(s: string): string { return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; }
|
|
|
|
/** 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<string, string>[] {
|
|
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';
|