Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CSV (std:csv)

Parse and generate delimiter-separated tabular text.

Overview

std:csv exports exactly two functions:

  • parse(text, delimiter = ",")List of lists of strings
  • generate(rows, delimiter = ",") → String

parse is purely positional: it does not interpret the first row as headers and never produces maps. Every cell comes back as a string.

Quick Start

import std:csv
import std:println

rows = csv:parse("name,age\nAlice,30\nBob,25\n")

println(rows::length())  # 3
println(rows[0])         # [name, age]
println(rows[1][0])      # Alice
println(rows[1][1]::to_number() + 1)  # 31

parse(text, delimiter = ",")

Returns a list of rows, each row a list of string cells. Quoted fields may contain the delimiter, embedded quotes and newlines; empty input yields an empty list, and blank trailing lines are ignored.

import std:csv
import std:println

println(csv:parse(""))                     # []
println(csv:parse("a,\"b,c\",d"))          # [[a, b,c, d]]
println(csv:parse("a,\"multi\nline\""))    # [[a, multi
                                           # line]]

Every row must have the same number of fields — a ragged row raises CSV parse error:

import std:csv

# csv:parse("a,b\nc")
# Error: CSV parse error: Invalid CSV: CSV error: record 1 (line: 2, byte: 4):
# found record with 1 fields, but the previous record has 2 fields

Other Delimiters

import std:csv
import std:println

println(csv:parse("a;b\n1;2", ";"))    # [[a, b], [1, 2]]
println(csv:parse("x\ty", "\t"))       # [[x, y]]

Using the Header Row

Convert rows to maps yourself when you want access by name:

import std:csv
import std:println

rows = csv:parse("name,age,city\nAlice,30,Boston\nBob,25,NYC\n")
header = rows[0]

records = rows[1;]::map(|row| {
    record = {}
    loop through 0..header::length() with i {
        record[header[i]] = row[i]
    }
    record
})

println(records::length())     # 2
println(records[0]:name)       # Alice
println(records[1]:city)       # NYC

Filtering and Aggregating

import std:csv
import std:println

rows = csv:parse("name,age\nAlice,30\nBob,17\nCara,42\n")
data = rows[1;]

adults = data::filter(|row| row[1]::to_number() >= 18)
println(adults::length())  # 2

ages = data::map(|row| row[1]::to_number())
println(ages::sum())       # 89
println(ages::max())       # 42

generate(rows, delimiter = ",")

Takes a list of lists and returns CSV text ending with a newline. Cells containing the delimiter, a quote or a newline are quoted and escaped automatically.

import std:csv
import std:println

text = csv:generate([["name", "age"], ["Alice", "30"], ["Bob", "25"]])
println(text::trim())

Output:

name,age
Alice,30
Bob,25
import std:csv
import std:println

println(csv:generate([["has \"quote\"", "and,comma"]])::trim())
# "has ""quote""","and,comma"

Every Cell Must Be a String

Numbers and booleans are rejected, so convert before generating:

import std:csv

# csv:generate([["Alice", 30]])
# Error: CSV generation error: csv:generate expects all cells to be strings
import std:csv
import std:println

people = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]

rows = [["name", "age"]]
loop through people with p {
    rows::push([p:name, p:age::to_string()])
}

println(csv:generate(rows)::trim())

Output:

name,age
Alice,30
Bob,25

Rows must be lists: a list of maps or a flat list of strings raises CSV generation error: csv:generate expects all rows to be lists.

Files

import std:csv
import std:io
import std:println

p = `mktemp`

out = io:open(p, true, true)
out::write(csv:generate([["name", "age"], ["Alice", "30"], ["Bob", "17"]]))
out::close()

f = io:open(p)
rows = csv:parse(f::read_all())
f::close()

adults = rows[1;]::filter(|row| row[1]::to_number() >= 18)
println(adults::length())  # 1
println(adults[0][0])      # Alice

Gotchas

  • parse returns lists, never maps: rows[0]:name is a type error. Index by position, or build maps from the header row as shown above.
  • All parsed cells are strings, including numeric columns; use ::to_number(), and validate first because an unparsable value is a runtime error.
  • All generated cells must already be strings.
  • Ragged rows fail on parse rather than being padded.
  • generate output ends with a newline; ::trim() it when printing inline.

See Also