SPB Git

Repository Public

csv.ts 2.8 KB 53 lines typescript

1// A real CSV reader: quoted fields, doubled quotes, separators and line breaks inside quotes, CR/LF/CRLF.23/** The separator that splits the first lines most consistently (commas, semicolons or tabs outside quotes). */4export function detectDelimiter(text: string): ',' | ';' | '\t' {5  const candidates = [',', ';', '\t'] as const;6  const perLine: Record<string, number[]> = { ',': [], ';': [], '\t': [] };7  let quoted = false, lines = 0;8  const counts: Record<string, number> = { ',': 0, ';': 0, '\t': 0 };9  const flush = () => { for (const c of candidates) perLine[c]!.push(counts[c]!); counts[','] = counts[';'] = counts['\t'] = 0; lines++; };10  for (let i = 0; i < text.length && lines < 20 && i < 200_000; i++) {11    const ch = text[i]!;12    if (ch === '"') quoted = !quoted;13    else if (!quoted && (ch === '\n' || ch === '\r')) { if (ch === '\r' && text[i + 1] === '\n') i++; flush(); }14    else if (!quoted && ch in counts) counts[ch]!++;15  }16  if (counts[','] || counts[';'] || counts['\t'] || lines === 0) flush();17  let best: ',' | ';' | '\t' = ',', bestScore = 0;18  for (const c of candidates) {19    const xs = perLine[c]!.filter((n, i, a) => n > 0 || i < a.length - 1);20    const total = xs.reduce((a, b) => a + b, 0);21    if (!total) continue;22    const same = xs.filter((n) => n === xs[0]).length / xs.length; // a real separator appears the same number of times on each line23    const score = total * (0.25 + same);24    if (score > bestScore) { best = c; bestScore = score; }25  }26  return best;27}2829export interface CsvTable { rows: string[][]; delimiter: string; more: boolean; columns: number }30/** Parse up to `maxRows` records; `more` tells that the text held further ones. */31export function parseCsv(text: string, maxRows: number, delimiter: string = detectDelimiter(text)): CsvTable {32  const rows: string[][] = [];33  let row: string[] = [], field = '', quoted = false, i = text.charCodeAt(0) === 0xfeff ? 1 : 0, columns = 0, more = false, started = false;34  const endRow = () => { row.push(field); field = ''; if (row.length > 1 || row[0] !== '' ) { rows.push(row); columns = Math.max(columns, row.length); } row = []; started = false; };35  for (; i < text.length; i++) {36    const ch = text[i]!;37    if (quoted) {38      if (ch === '"') { if (text[i + 1] === '"') { field += '"'; i++; } else quoted = false; }39      else field += ch;40      continue;41    }42    if (ch === '"' && field === '') { quoted = true; started = true; }43    else if (ch === delimiter) { row.push(field); field = ''; started = true; }44    else if (ch === '\n' || ch === '\r') {45      if (ch === '\r' && text[i + 1] === '\n') i++;46      endRow();47      if (rows.length >= maxRows) { more = /\S/.test(text.slice(i + 1, i + 4096)); break; }48    } else { field += ch; started = true; }49  }50  if (!more && (started || field !== '' || row.length)) endRow();51  return { rows, delimiter, more, columns };52}53
Files main
Menu

Theme