typescript 61 lines · 8 steps

Parsing CSV with a character state machine

A single-pass scanner tracks quote state character by character to correctly split CSV into rows, fields, and typed records.

Explained by highlit
1type CsvRecord = Record<string, string>;
2 
3function parseCsv(input: string): CsvRecord[] {
4 const rows: string[][] = [];
5 let field = "";
6 let row: string[] = [];
7 let inQuotes = false;
8 
9 for (let i = 0; i < input.length; i++) {
10 const char = input[i];
11 
12 if (inQuotes) {
13 if (char === '"') {
14 if (input[i + 1] === '"') {
15 field += '"';
16 i++;
17 } else {
18 inQuotes = false;
19 }
20 } else {
21 field += char;
22 }
23 continue;
24 }
25 
26 switch (char) {
27 case '"':
28 inQuotes = true;
29 break;
30 case ",":
31 row.push(field);
32 field = "";
33 break;
34 case "\r":
35 break;
36 case "\n":
37 row.push(field);
38 rows.push(row);
39 field = "";
40 row = [];
41 break;
42 default:
43 field += char;
44 }
45 }
46 
47 if (field.length > 0 || row.length > 0) {
48 row.push(field);
49 rows.push(row);
50 }
51 
52 const [header, ...body] = rows;
53 if (!header) return [];
54 
55 return body.map((cells) =>
56 header.reduce<CsvRecord>((record, key, index) => {
57 record[key] = cells[index] ?? "";
58 return record;
59 }, {})
60 );
61}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Tracking a small amount of state like inQuotes turns naive splitting into a correct parser that handles edge cases.
  2. 2Quoted fields need special handling for embedded delimiters and doubled quotes as escaped literals.
  3. 3Separating tokenization from record assembly keeps each phase simple and easy to reason about.

Related explainers

typescript
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import localeFrExtra from '@angular/common/locales/extra/fr';
import localeDe from '@angular/common/locales/de';

Locale-aware bootstrapping in Angular

i18n localization dependency-injection
Intermediate 8 steps
typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
 

Validating env config at boot in NestJS

configuration schema-validation environment-variables
Intermediate 8 steps
ruby
class UserAgentParser
  BROWSERS = [
    [/Edg\/([\d.]+)/, "Edge"],
    [/OPR\/([\d.]+)/, "Opera"],

Parsing user-agent strings in Ruby

regex pattern-matching lookup-tables
Intermediate 8 steps
javascript
function evaluate(expression) {
  const tokens = tokenize(expression);
  let pos = 0;
 

Building a recursive descent calculator

parsing recursion operator-precedence
Intermediate 8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { InjectRepository } from '@nestjs/typeorm';

A cache-aside country lookup in NestJS

cache-aside dependency-injection batch-lookup
Intermediate 8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core';
 
interface Preferences {
  theme: 'light' | 'dark';

A signal-based preferences store in Angular

signals state-management persistence
Intermediate 7 steps

Share this explainer

Here's the card — post it anywhere.

Parsing CSV with a character state machine — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code