diff --git a/package.json b/package.json index 6815e5c..c852be0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/utils", - "version": "0.23.16", + "version": "0.23.17", "description": "Utility library", "author": "Zak Timson", "license": "MIT", diff --git a/src/csv.ts b/src/csv.ts index dfc85b8..997f31a 100644 --- a/src/csv.ts +++ b/src/csv.ts @@ -10,16 +10,16 @@ import {LETTER_LIST} from './string.ts'; * @param hasHeaders First line of CSV contains headers * @return {T[]} Array of parsed objects */ -export function fromCsv(csv: string, hasHeaders=true): T[] { +export function fromCsv(csv: string, hasHeaders = true): T[] { function parseLine(line: string): (string | null)[] { const columns: string[] = []; let current = '', inQuotes = false; - for(let i = 0; i < line.length; i++) { + for (let i = 0; i < line.length; i++) { const char = line[i]; const nextChar = line[i + 1]; if (char === '"') { if (inQuotes && nextChar === '"') { - current += '"'; + current += '"'; // Handle escaped quotes i++; } else inQuotes = !inQuotes; } else if (char === ',' && !inQuotes) { @@ -31,15 +31,29 @@ export function fromCsv(csv: string, hasHeaders=true): T[] { return columns.map(col => col.replace(/^"|"$/g, '').replace(/""/g, '"')); } - const row = csv.split(/\r?\n/); - let headers: any = hasHeaders ? row.splice(0, 1)[0] : null; - if(headers) headers = headers.match(/(?:[^,"']+|"(?:[^"]|"")*"|'(?:[^']|'')*')+/g); - return row.map(r => { + // Split rows + const rows = []; + let currentRow = '', inQuotes = false; + for (const char of csv) { + if (char === '"') inQuotes = !inQuotes; + if (char === '\n' && !inQuotes) { + rows.push(currentRow); + currentRow = ''; + } else currentRow += char; + } + if(currentRow) rows.push(currentRow); + + // Figure out headers + let headers: any = hasHeaders ? rows.splice(0, 1)[0] : null; + if (headers) headers = headers.match(/(?:[^,"']+|"(?:[^"]|"")*"|'(?:[^']|'')*')+/g); + + // Parse rows + return rows.map(r => { const props = parseLine(r); - const h = headers || (Array(props.length).fill(null).map((r, i) => { + const h = headers || (Array(props.length).fill(null).map((_, i) => { let letter = ''; const first = i / 26; - if(first > 1) letter += LETTER_LIST[Math.floor(first - 1)]; + if (first > 1) letter += LETTER_LIST[Math.floor(first - 1)]; letter += LETTER_LIST[i % 26]; return letter; })); @@ -65,7 +79,7 @@ export function toCsv(target: any, flatten=true) { ...t.map(row => headers.map((h: string) => { const value = dotNotation(row, h); if(value == null) return ''; - if(typeof value == 'object') return `"${JSONSanitize(value).replaceAll('`', '""')}"`; + if(typeof value == 'object') return `"${JSONSanitize(value).replaceAll('"', '""')}"`; if(typeof value == 'string' && /[\n"]/g.test(value)) return `"${value.replaceAll('"', '""')}"`; return value; }).join(','))