Hand-roll a CSV parser for the member-import dialog
Angular · Angular · advanced · modification
Replaces the old one-line `text.split('\n').map(l => l.split(','))` importer with a proper hand-rolled CSV parser. It normalises Windows CRLF and old-Mac CR endings to `\n`, skips blank and spacer lines, trims the padding Sheets leaves around cells, and tolerates ragged rows so a short line no longer throws. Quoted fields and the UTF-8 BOM that Excel and Google Sheets prepend are both accounted for. Spot-checked against a real export from Google Sheets and the member list imported cleanly.
The parser backs the "Import members" dialog on the team-settings page. Admins upload CSVs exported straight from Excel or Google Sheets, and member names with a comma ("Last, First") are common in the data.
Requirements
- Parse the uploaded CSV into one record per data row, keyed by the header names on the first row; the "Import members" dialog feeds each record straight into the create-member call, so every cell must land under the right column.
- Exports from Excel and Google Sheets save UTF-8 CSVs with a byte-order mark (BOM, U+FEFF) at the very start of the file. Files with a BOM must parse identically to files without one: the first column's header must be `id`, not a BOM-prefixed variant, so the id column maps and its values are kept.
- A quoted field may itself contain commas — a member `name` of `"Smith, John"` is one value, not two. Commas inside double quotes are part of the value and must not split the field into extra columns.
- Keep the surrounding hygiene intact: trim surrounding spaces/tabs from every cell, accept both LF and CRLF line endings, skip blank lines anywhere in the file, and tolerate ragged rows (a short row leaves trailing columns empty; cells beyond the header count are ignored).
Files touched
- src/app/import/csv-parser.service.ts
--- src/app/import/csv-parser.service.ts
* Parses the CSV pasted or uploaded in the "Import members" dialog into one
* record per data row, keyed by header name.
+ *
+ * Replaces the previous one-line `text.split('\n').map(l => l.split(','))`
+ * importer with a hand-rolled parser that tolerates the exports we actually
+ * receive: CRLF line endings from Windows, blank spacer lines, cells padded
+ * with spaces, and rows with fewer or more columns than the header row.
*/
@Injectable({ providedIn: 'root' })
export class CsvParserService {
+ /**
+ * Parse a raw CSV document. The first non-blank line is the header; every
+ * later non-blank line becomes one record.
+ */
parse(text: string): CsvRow[] {
- const lines = text.split('\n');
- const headers = lines[0].split(',');
- return lines.slice(1).map((line) => {
- const cells = line.split(',');
- const row: CsvRow = {};
- headers.forEach((header, i) => {
- row[header] = cells[i];
- });
- return row;
- });
+ // Normalise Windows (CRLF) and old-Mac (CR) line endings to a single \n so
+ // the row loop below only ever has to deal with one separator.
+ const normalised = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
+ const lines = normalised.split('\n');