Skip to main content
Back to BlogConverter Guides

How to Edit CSV Files (and Why Quoted Commas Break the Naive Split)

Edit CSV files in your browser with a table interface — and learn why the naive comma split breaks on quoted fields, why RFC 4180 doubles quotes instead of escaping them, why numeric sort and string sort disagree, why empty cells matter for stats, and why CSV-to-JSON is a one-liner with Object.fromEntries.

The Toolbox TeamAugust 14, 20267 min read

The problem: you opened a CSV in a text editor, changed a cell, and broke the file

You have a 500-row CSV. You open it in Notepad to fix a typo, find the cell, type the correction, save. The next time a script reads the file, the row count is off by one — because the cell you edited contained a comma, and the original file had that comma wrapped in quotes ("Smith, John"). By replacing the cell contents without the quotes, you turned one field into two, and the parser sees an extra column on that row. The naive approach — treating CSV as "split by comma, join by comma" — breaks the moment a field contains a comma, a quote, or a newline. The honest move is a parser that respects RFC 4180 quoted-field semantics, an editor that renders the parsed rows as an editable table, and an exporter that re-quotes only when needed on the way out.

Fastest path

Open the CSV Editor, paste the CSV text (or upload the file), click Load into Editor, edit cells in the table.

Input (paste):
  name,age,email,city,score
  Alice,29,alice@example.com,New York,92
  Bob,34,"Smith, Bob",London,78

→ Parsed: 5 columns, 3 rows
→ "Smith, Bob" stays as ONE cell (quoted comma preserved)
→ Sort by score desc:  Eve(90) → Alice(92) → Bob(78) ... wait, numeric sort
→ Search "lon": 1 row matches
→ Export CSV:  re-quoted only where needed
→ Export JSON:  [{"name":"Alice","age":"29",...}, ...]

The tool parsed the CSV with a state-machine that tracks inQuote, rendered the rows as an editable table, let you sort and filter without touching the raw text, and exported back to CSV or JSON with correct re-quoting. The rest of this guide is why the naive split breaks, why RFC 4180 doubles quotes, why numeric and string sort disagree, why empty cells matter, and why CSV-to-JSON is one line of Object.fromEntries.

The substance: one parser, one state machine, one re-quote pass

The naive split breaks on the first quoted comma

The tool's parseCsv function does not use line.split(','). It walks each line character by character with a state machine: inQuote is a boolean that flips when the parser hits a ". Inside quotes, commas are literal characters, not field separators. Outside quotes, a comma ends the current cell and starts a new one. The loop:

for (let i = 0; i < line.length; i++) {
  const ch = line[i];
  if (ch === '"') {
    if (inQuote && line[i + 1] === '"') { cell += '"'; i++; }   // doubled quote → literal "
    else { inQuote = !inQuote; }                                  // toggle quote state
  } else if (ch === ',' && !inQuote) {
    cells.push(cell); cell = '';                                  // field separator
  } else {
    cell += ch;                                                    // literal char
  }
}
cells.push(cell);                                                  // last cell

A line like "Smith, Bob",34,"London, UK" parses as three cells: Smith, Bob, 34, London, UK. The naive split(',') would parse it as five cells: "Smith, Bob", 34, "London, UK" — broken. The state machine is the only correct way to parse CSV; any parser that uses split is wrong on the first quoted comma.

RFC 4180 doubles quotes instead of escaping them

CSV's quoting rule, per RFC 4180: a field is wrapped in double quotes when it contains a comma, a double quote, or a newline. A literal double quote inside a quoted field is represented as two double quotes in a row — "" — not as \". This is why the parser's if (inQuote && line[i + 1] === '"') branch exists: it sees the doubled quote, emits one literal ", and skips the second one. The alternative — backslash escaping — is not RFC 4180; it's a Unix convention that CSV doesn't use.

The tool's toCsvString function re-quotes on export only when needed: if (c.includes(',') || c.includes('"') || c.includes('\n')) — wrap in quotes, and replace every " with "". A cell like Hello exports as Hello (no quotes). A cell like Hello, World exports as "Hello, World". A cell like Say "hi" exports as "Say ""hi""". The re-quote pass is idempotent: parse the output, get the same rows back.

Numeric sort vs string sort and the localeCompare fallback

The tool's handleSort cycles through three states on each column click: ascending, descending, off. The filtered memo applies the sort with a type-aware comparator:

const an = parseFloat(av);
const bn = parseFloat(bn);
const numeric = !isNaN(an) && !isNaN(bn);
const cmp = numeric ? an - bn : av.localeCompare(bv);
return dir === 'asc' ? cmp : -cmp;

If both values parse as numbers (parseFloat returns a non-NaN), the comparator subtracts them — numeric sort, so 2 comes before 10. If either value is non-numeric, the comparator falls back to localeCompare — string sort, which is locale-aware and handles accented characters correctly (é sorts near e, not near z).

The type-aware split matters because string sort on numbers is wrong: '2' < '10' is false in lexicographic order (because '2' > '1'), so a string sort puts 2 after 10. A numeric sort puts 2 before 10. The tool's check !isNaN(an) && !isNaN(bn) runs per pair — if a column is all numbers, every comparison is numeric; if it's mixed, the numeric pairs sort numerically and the non-numeric pairs sort by locale. This is the right behavior for a CSV editor that doesn't know your schema ahead of time.

Empty cells and why they matter for stats

The tool's stats memo counts three things: row count, column count, and empty cells. The empty-cell count is rows.reduce((acc, r) => acc + r.filter((c) => !c.trim()).length, 0) — for each row, count cells where the trimmed value is empty, and sum across rows. The badge turns red (destructive variant) when there are any empty cells.

Why this matters: empty cells in a CSV are ambiguous. They could mean "missing data," "not applicable," or "zero, but the export dropped it." A data analyst reading the CSV needs to know which rows have gaps before running stats or joins. The empty-cell count is a quick integrity check — if you expect 500 complete rows and the tool shows 12 empty cells, you have 12 fields to fill before the data is usable. The red badge is a visual prompt to look at the gaps before exporting.

CSV-to-JSON and Object.fromEntries

The tool's handleDownloadJson function is one line of meaningful code:

rows.map((r) => Object.fromEntries(headers.map((h, i) => [h, r[i] ?? ''])))

For each row, headers.map((h, i) => [h, r[i] ?? '']) builds an array of [header, cell] pairs. Object.fromEntries turns that array into an object: {name: 'Alice', age: '29', email: 'alice@example.com', ...}. The ?? '' fallback handles rows that are shorter than the header row (missing trailing cells become empty strings, not undefined).

This is the cleanest CSV-to-JSON conversion because CSV's structure is already an array of arrays — the only transformation is pairing each cell with its column header. The JSON.stringify(..., null, 2) call pretty-prints with 2-space indentation. The output is an array of objects, one per row, with headers as keys — the shape most APIs and databases expect.

The two-mode UX: paste vs table

The tool has two modes: paste (a textarea for raw CSV input) and table (the editable grid). Paste mode is for input — you paste or upload, click Load into Editor, and the parser converts raw text to rows. Table mode is for editing — you change cells, add/delete rows and columns, sort, filter. The "Edit Raw" button switches back to paste mode with the current state serialized as CSV in the textarea.

The two-mode split exists because editing raw CSV text is error-prone (you can break quoting by hand) and editing a table is constrained (you can't break quoting because the table doesn't show quotes — it shows parsed values). The table is the safe editing surface; the raw view is for bulk paste or inspection. Switching between them is lossless because toCsvString and parseCsv are inverse operations (assuming RFC 4180 compliance).

Gotchas

  • The naive split(',') breaks on quoted commas. "Smith, Bob" becomes two cells instead of one. Use a state-machine parser that tracks inQuote, or use a tool that does. The tool's parseCsv is the correct implementation — don't replace it with split.
  • RFC 4180 doubles quotes, doesn't backslash-escape them. A literal " inside a quoted field is "", not \". The parser's if (inQuote && line[i + 1] === '"') branch handles this. If you're hand-editing raw CSV, use "" for literal quotes.
  • String sort on numbers is wrong. '2' > '10' in lexicographic order. The tool's sort checks parseFloat and uses numeric comparison when both values are numbers. If your column has mixed types, the numeric pairs sort numerically and the rest sorts by localeCompare.
  • Empty cells are ambiguous. They could mean missing, N/A, or zero. The tool's empty-cell badge is a prompt to investigate before exporting. Don't assume empty means zero — it might mean the data wasn't collected.
  • The parser is line-based, not RFC 4180-compliant for newlines in fields. The tool splits on /\r?\n/ first, then parses each line. A quoted field that contains a newline (allowed by RFC 4180) will be split across two "lines" and parsed incorrectly. If your CSV has embedded newlines, use a tool that parses with a character-level state machine across the whole file, not a line-based parser.
  • The re-quote pass is conservative. toCsvString quotes a field only if it contains a comma, quote, or newline. It doesn't quote fields with leading/trailing spaces, which some parsers strip. If your downstream tool is picky about whitespace, add quotes manually.
  • Adding a column fills existing rows with empty strings. The addColumn function pushes a new header and appends '' to every existing row. The empty-cell count will jump by the row count. Fill the new column before exporting, or accept the empty cells.
  • CSV-to-JSON uses headers as keys. If your headers have duplicates (two columns named name), Object.fromEntries will keep only the last value for each key — the first name column is silently dropped. Deduplicate headers before exporting to JSON.
  • The search is case-insensitive substring match. r.some((c) => c.toLowerCase().includes(q.toLowerCase())) — any cell in a row containing the query (case-insensitive) matches the row. There's no regex, no whole-word, no column-specific search. For complex filtering, export and use a spreadsheet.

Summary

  • The naive split(',') breaks on quoted commas. "Smith, Bob" is one cell, not two. The tool's parseCsv uses a character-by-character state machine that tracks inQuote — the only correct way to parse CSV. Any parser that uses split is wrong on the first quoted comma.
  • RFC 4180 doubles quotes, doesn't escape them. A literal " inside a quoted field is "". The parser's doubled-quote branch (if (inQuote && line[i + 1] === '"')) handles this. The toCsvString exporter re-quotes only when a field contains a comma, quote, or newline — and doubles any quotes inside.
  • Numeric sort and string sort disagree. String sort puts 2 after 10 (lexicographic). The tool's sort checks parseFloat on both values and uses numeric comparison when both are numbers, falling back to localeCompare for strings. This is the right behavior for schema-agnostic CSV editing.
  • Empty cells matter for stats. The tool counts empty cells and shows a red badge when any exist. Empty is ambiguous (missing, N/A, zero) — investigate before exporting. The count is a quick integrity check.
  • CSV-to-JSON is one line: Object.fromEntries(headers.map((h, i) => [h, r[i] ?? ''])). Each row becomes an object with headers as keys. The ?? '' fallback handles short rows. Duplicate headers silently drop earlier columns — deduplicate first.
  • Two modes: paste (raw input) and table (safe editing). The table doesn't show quotes — it shows parsed values — so you can't break quoting by editing a cell. Switch to paste mode for bulk input or inspection. The round-trip is lossless because parseCsv and toCsvString are inverse operations.
  • Edit at the CSV Editor; for merging multiple CSVs use CSV Merger, for converting to spreadsheet use CSV to Excel Converter, and for the reverse JSON conversion use JSON to CSV Converter.