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

ruby
class Order
  class InvalidTransition < StandardError; end
 
  TRANSITIONS = {

A state machine for order transitions in Ruby

state-machine data-driven error-handling
Intermediate 8 steps
typescript
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';

How a JWT strategy authenticates in NestJS

authentication jwt passport
Intermediate 7 steps
python
import time
import threading
from enum import Enum
from functools import wraps

Building a circuit breaker in Python

circuit-breaker resilience decorators
Advanced 7 steps
java
public final class Slugifier {
 
    private static final Pattern NON_LATIN = Pattern.compile("[^\\w-]");
    private static final Pattern WHITESPACE = Pattern.compile("[\\s]+");

Building a URL slugifier in Java

regex unicode-normalization string-processing
Intermediate 8 steps
typescript
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';

Refreshing auth tokens in an Angular interceptor

http-interceptor token-refresh rxjs
Advanced 8 steps
rust
use std::cmp::Ordering;
use std::str::FromStr;
 
#[derive(Debug, Clone, PartialEq, Eq)]

Parsing and ordering semantic versions in Rust

parsing trait-implementation ordering
Intermediate 8 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