← back to Nationalrealestate
src/ingest/brokers/csv.ts
52 lines
/**
* Minimal RFC-4180 CSV parser (quoted fields, embedded commas/newlines/"" escapes).
* Whole-file in memory — largest M-B1 file is ~65MB, fine on a 32GB workstation.
*/
import { readFileSync } from 'node:fs';
export function parseCsv(text: string): string[][] {
const rows: string[][] = [];
let row: string[] = [];
let field = '';
let inQ = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (inQ) {
if (c === '"') {
if (text[i + 1] === '"') { field += '"'; i++; }
else inQ = false;
} else field += c;
} else if (c === '"') inQ = true;
else if (c === ',') { row.push(field); field = ''; }
else if (c === '\n') { row.push(field); field = ''; rows.push(row); row = []; }
else if (c !== '\r') field += c;
}
if (field !== '' || row.length) { row.push(field); rows.push(row); }
return rows;
}
export function parseCsvFile(path: string, encoding: BufferEncoding = 'utf8'): string[][] {
return parseCsv(readFileSync(path, { encoding }));
}
/** Header display names -> snake_case keys ("License Type" -> license_type). */
export function headerKeys(header: string[]): string[] {
return header.map(h => h.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, ''));
}
/** Parse a headered CSV file into objects keyed by snake_cased header names. */
export function parseCsvObjects(path: string, encoding: BufferEncoding = 'utf8'): Record<string, string>[] {
const rows = parseCsvFile(path, encoding);
if (!rows.length) return [];
const keys = headerKeys(rows[0]);
const out: Record<string, string>[] = [];
for (let i = 1; i < rows.length; i++) {
const r = rows[i];
if (r.length === 1 && r[0] === '') continue;
const o: Record<string, string> = {};
for (let j = 0; j < keys.length; j++) o[keys[j]] = (r[j] ?? '').trim();
out.push(o);
}
return out;
}