Better CSV handling
All checks were successful
Build / Build NPM Project (push) Successful in 1m15s
Build / Tag Version (push) Successful in 16s
Build / Publish Documentation (push) Successful in 53s

This commit is contained in:
Zakary Timson 2025-03-24 21:28:51 -04:00
parent 1c2c18b65d
commit e0085ecb6f
2 changed files with 41 additions and 24 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@ztimson/utils", "name": "@ztimson/utils",
"version": "0.23.15", "version": "0.23.16",
"description": "Utility library", "description": "Utility library",
"author": "Zak Timson", "author": "Zak Timson",
"license": "MIT", "license": "MIT",

View File

@ -1,27 +1,40 @@
import {makeArray} from './array.ts';
import {ASet} from './aset.ts'; import {ASet} from './aset.ts';
import {dotNotation, flattenObj, JSONSanitize} from './objects.ts'; import {dotNotation, flattenObj, JSONSanitize} from './objects.ts';
import {LETTER_LIST} from './string.ts'; import {LETTER_LIST} from './string.ts';
/**
* Parse a CSV string into an array of objects
*
* @param csv String with CSV
* @param hasHeaders First line of CSV contains headers
* @return {T[]} Array of parsed objects
*/
export function fromCsv<T = any>(csv: string, hasHeaders=true): T[] { export function fromCsv<T = any>(csv: string, hasHeaders=true): T[] {
const row = csv.split('\n'); function parseLine(line: string): (string | null)[] {
let headers: any = hasHeaders ? row.splice(0, 1)[0] : null; const columns: string[] = [];
if(headers) headers = headers.match(/(?:[^,"']+|"[^"]*"|'[^']*')+/g); let current = '', inQuotes = false;
return <T[]>row.map(r => { for(let i = 0; i < line.length; i++) {
function parseLine(line: string): (string | null)[] { const char = line[i];
const parts = line.split(','), columns: string[] = []; const nextChar = line[i + 1];
let quoted = false; if (char === '"') {
for(const p of parts) { if (inQuotes && nextChar === '"') {
if(quoted) columns[columns.length - 1] = columns.at(-1) + ',' + p; current += '"';
else columns.push(p); i++;
if(/[^"]"$/g.test(p)) { } else inQuotes = !inQuotes;
quoted = false; } else if (char === ',' && !inQuotes) {
} else if(/^"[^"]/g.test(p)) { columns.push(current);
quoted = true; current = '';
} } else current += char;
}
return columns;
} }
columns.push(current);
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 <T[]>row.map(r => {
const props = parseLine(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((r, i) => {
let letter = ''; let letter = '';
@ -34,23 +47,27 @@ export function fromCsv<T = any>(csv: string, hasHeaders=true): T[] {
dotNotation(acc, h, props[i]); dotNotation(acc, h, props[i]);
return acc; return acc;
}, {}); }, {});
}) });
} }
/** /**
* Convert an object to a CSV string * Convert an array of objects to a CSV string
* *
* @param {any[]} target Array of objects to create CSV from * @param {any[]} target Array of objects to create CSV from
* @param {boolean} flatten Should nested object be flattened or treated as values * @param {boolean} flatten Should nested object be flattened or treated as values
* @return {string} CSV string * @return {string} CSV string
*/ */
export function toCsv(target: any[], flatten=true) { export function toCsv(target: any, flatten=true) {
const headers = new ASet(target.reduce((acc, row) => [...acc, ...Object.keys(flatten ? flattenObj(row) : row)], [])); const t = makeArray(target);
const headers = new ASet(t.reduce((acc, row) => [...acc, ...Object.keys(flatten ? flattenObj(row) : row)], []));
return [ return [
headers.join(','), headers.join(','),
...target.map(row => headers.map((h: string) => { ...t.map(row => headers.map((h: string) => {
const value = dotNotation<any>(row, h); const value = dotNotation<any>(row, h);
return (typeof value == 'object' && value != null) ? '"' + JSONSanitize(value).replaceAll('"', '""') + '"' : value; if(value == null) return '';
if(typeof value == 'object') return `"${JSONSanitize(value).replaceAll('`', '""')}"`;
if(typeof value == 'string' && /[\n"]/g.test(value)) return `"${value.replaceAll('"', '""')}"`;
return value;
}).join(',')) }).join(','))
].join('\n'); ].join('\n');
} }