Standard library · Encoding and data

encoding/csv

Reading and writing CSV files, with correct quoting and conversion to a dynamic value.

import encoding.csv

CSV is the simplest table format: one row per line, fields separated by commas. A field containing a comma, a quote or a line break is wrapped in quotes, and a quote inside it is doubled. This package reads and writes that format, one line at a time with ReadLine and WriteLine or a whole text with ReadAll and WriteAll, and converts it to and from a Value for encoding/json.

Every field is a string; CSV has no numbers or booleans. For spreadsheets with several sheets, formulas or types, see excel; for CSV that feeds a machine learning model, data.

The things people get wrong:

  • The separator is always a comma. A file from a European Excel with ; between fields, or a tab-separated file, reads as one field per line; replace the separator or split those lines yourself.
  • A quoted field must be followed directly by a comma or the end of the line. In "Pen" ,blue the space after the quote ends the row, and the rest of the line is dropped without an error.
  • Line breaks inside a quoted field cannot be read back: ReadAll splits the text into lines first. WriteLine quotes such a field correctly for other tools, but this reader cuts it in two.
  • FromValue takes the columns from the keys of the first object, in the order they were added, so a table read with ToValue is written back with its columns in the original order.

Guide: totals from a CSV export

The typical job is a spreadsheet export: read every row, skip the header, convert the number columns and add them up. ReadAll gives a CSVTable whose rows each hold a fields vector, one string per column. A HashMap keeps the totals and a Vector keeps the cities in the order they first appeared.

encoding-csv-guide-report.salam
import encoding.csv

func main:
    text := `date,city,product,amount
2026-10-01,Tehran,tea,120
2026-10-01,Shiraz,coffee,90
2026-10-02,Tehran,coffee,75
2026-10-02,Tabriz,tea,40
2026-10-03,Shiraz,tea,60
`
    mut table := csv.ReadAll(text)
    mut cities := Vector {} as Vector<str>
    mut totals := HashMap {} as HashMap<str, int>
    mut grand := 0
    each i, row in table.rows:
        if i == 0 or row.fields.len() < 4:
            continue
        end
        city := row.fields.get(1)
        amount := row.fields.get(3).to_int()
        if not totals.has(city):
            cities.push(city)
        end
        totals.put(city, totals.get_or(city, 0) + amount)
        grand += amount
    end
    each c in cities:
        println c, totals.get(c)
    end
    println "total", grand
    csv.FreeTable(table)
end
OutputTehran 195 Shiraz 150 Tabriz 40 total 385

Guard against short rows, as the row.fields.len() < 4 check does: a blank line or a hand-edited file can have fewer columns than the header. Free the table with FreeTable when you are done.

Guide: write a CSV file

WriteLine joins one row and adds quotes only where they are needed: around a field with a comma, a quote or a line break, with inner quotes doubled. That is what Excel, LibreOffice and most import tools expect, and writing the rows yourself keeps the columns in the order you choose.

encoding-csv-guide-export.salam
import encoding.csv

struct Contact:
    pub name: str = ""
    pub company: str = ""
    pub note: str = ""
end

func main:
    mut people := Vector {} as Vector<Contact>
    people.push(Contact { name = "Sara Ahmadi", company = "Nik, Ltd.", note = "prefers email" })
    people.push(Contact { name = "Reza", company = "Pars", note = `said "call me Monday"` })

    mut out := ""
    mut header := Vector {} as Vector<str>
    header.push("name")
    header.push("company")
    header.push("note")
    out = out + csv.WriteLine(header) + "\n"
    each p in people:
        mut f := Vector {} as Vector<str>
        f.push(p.name)
        f.push(p.company)
        f.push(p.note)
        out = out + csv.WriteLine(f) + "\n"
    end
    print out

    row := csv.ReadLine(`Reza,Pars,"said ""call me Monday"""`)
    println "read back:", row.get(2)
end
Outputname,company,note Sara Ahmadi,"Nik, Ltd.",prefers email Reza,Pars,"said ""call me Monday""" read back: said "call me Monday"

Write the header row the same way as the data rows. WriteLine does not add the line break, so add "\n" yourself, or "\r\n" for tools that insist on it. To save the result, write out with fs/file.

Guide: CSV to JSON

ToValue uses the header row as keys and turns every other row into an object, which encoding/json can then print. That is a small CSV to JSON converter in a few lines. FromValue goes back from an array of objects to CSV text.

encoding-csv-guide-json.salam
import encoding.csv
import encoding.json
import encoding.value

func main:
    text := "sku,title,stock\nBK-01,Notebook,12\nPN-07,\"Pen, blue\",0\n"
    rows := csv.ToValue(text)
    println value.ArrayLen(rows), "rows"
    opts := json.JSONOptions { indent = "  ", sort_keys = true }
    println json.Encode(rows, opts)

    first := value.ArrayGet(rows, 0)
    println "stock is a string:", value.IsString(value.ObjectGet(first, "stock"))
end
Output2 rows [ { "sku": "BK-01", "stock": "12", "title": "Notebook" }, { "sku": "PN-07", "stock": "0", "title": "Pen, blue" } ] stock is a string: true

Every field stays a string, even 12, because CSV has no types. Convert the columns you know are numbers with to_int() or to_float(). With CSVOptions { header = false }, ToValueWith reads a file without a header: every row, the first one included, becomes an object with the keys col0, col1 and so on.

Guide: edge cases

The reader keeps fields exactly as they are written. That matters in a few places:

encoding-csv-guide-pitfalls.salam
import encoding.csv

func show(line: str):
    f := csv.ReadLine(line)
    mut s := ""
    each x in f:
        s = s + "[" + x + "]"
    end
    println f.len(), "fields:", s
end

func main:
    show("a,b,c")
    show("a,,c")
    show(" a , b ")
    show(`"x,y",z`)
    show(`"she said ""hi""",ok`)
    show("trailing,")
    show(`"Pen" ,blue,3`)
    show("sku;title;stock")

    mut t := csv.ReadAll("id,name\r\n1,Ali\r\n2,Sara\r\n")
    println "rows with CRLF:", t.rows.len(), t.rows.get(2).fields.get(1)
    csv.FreeTable(t)
end
Output3 fields: [a][b][c] 3 fields: [a][][c] 2 fields: [ a ][ b ] 2 fields: [x,y][z] 2 fields: [she said "hi"][ok] 2 fields: [trailing][] 1 fields: [Pen] 1 fields: [sku;title;stock] rows with CRLF: 3 Sara
  • An empty field, including one after a trailing comma, is an empty string, not a missing column.
  • Spaces around a field are kept; trim them with str.Trim if your source pads columns.
  • Windows line endings (\r\n) are handled by ReadAll.
  • ReadAll skips blank lines, so a table never has empty rows.
  • A space between a closing quote and the comma ends the row: "Pen" ,blue,3 gives one field.
  • A line with semicolons is one field, because only commas separate fields.
  • ReadAll splits the text into lines before it reads the fields, so a quoted field that contains a line break comes back cut in two. If your data can hold line breaks, replace them before writing the file.

Types

CSVRow

struct CSVRow
One row.
NameTypeDescription
fieldsVectorThe row's fields.
encoding-csv-Table.salam
import encoding.csv
import encoding.value

func main:
    text := "name,city\nAli,Tehran\n\"Sara, engineer\",\"Shiraz\"\n"
    mut table := csv.ReadAll(text)
    println table.rows.len()
    each row in table.rows:
        println row.fields.len(), row.fields[0]
    end
    println csv.WriteAll(table)
    csv.FreeTable(table)
    println csv.ReadLine(`a,"b,c",d`)
    mut fields := Vector {} as Vector<str>
    fields.push("plain")
    fields.push(`with "quotes"`)
    println csv.WriteLine(fields)
    v := csv.ToValue("name,age\nAli,30\n")
    println value.ArrayLen(v), value.AsString(value.ObjectGet(value.ArrayGet(v, 0), "name"))
    println csv.FromValue(v)
    opts := csv.CSVOptions { header = false }
    rows := csv.ToValueWith("1,2\n3,4\n", opts)
    println value.ArrayLen(rows), csv.FromValueWith(rows, opts)
end
Output3 2 name 2 Ali 2 Sara, engineer name,city Ali,Tehran "Sara, engineer",Shiraz ["a", "b,c", "d"] plain,"with ""quotes""" 1 Ali name,age Ali,30 2 1,2 3,4

CSVTable

struct CSVTable
A table.
NameTypeDescription
rowsVectorThe rows, including the header row; blank lines are not included.

The example for this item is shown with the ones above: encoding-csv-Table.salam

CSVOptions

struct CSVOptions
The options for converting to and from a value.
NameTypeDescription
headerbool = trueThe first row is a header; true by default. When false, every row becomes an object with the keys col0, col1 and so on, and FromValueWith writes no header row.

The example for this item is shown with the ones above: encoding-csv-Table.salam

Functions

ReadAll

func ReadAll(text: str): CSVTable
Turns a whole piece of CSV text into a table. Both \n and \r\n line ends work and blank lines are skipped. Free the result with FreeTable.

The example for this item is shown with the ones above: encoding-csv-Table.salam

FreeTable

func FreeTable(t &: CSVTable)
Releases the table's memory.

The example for this item is shown with the ones above: encoding-csv-Table.salam

WriteAll

func WriteAll(table: CSVTable): str
Turns a table into CSV text, with \r\n between rows and no line break after the last one.

The example for this item is shown with the ones above: encoding-csv-Table.salam

ToValueWith

func ToValueWith(text: str, opts: CSVOptions): value.Value
Turns CSV into a Value with the given options.

The example for this item is shown with the ones above: encoding-csv-Table.salam

ToValue

func ToValue(text: str): value.Value
Turns CSV with a header into an array of objects, one per data row, with every value a string.

The example for this item is shown with the ones above: encoding-csv-Table.salam

FromValueWith

func FromValueWith(v: value.Value, opts: CSVOptions): str
Turns an array of objects into CSV with the given options. The columns are the keys of the first object, in no fixed order.

The example for this item is shown with the ones above: encoding-csv-Table.salam

FromValue

func FromValue(v: value.Value): str
Turns an array of objects into CSV with a header row. The column order is not fixed; use WriteLine when it matters.

The example for this item is shown with the ones above: encoding-csv-Table.salam

ReadLine

func ReadLine(line: str): Vector<str>
Splits one CSV line into its fields, undoing quotes and doubled quotes. Spaces are kept, and text after a closing quote that is not a comma is dropped.

The example for this item is shown with the ones above: encoding-csv-Table.salam

WriteLine

func WriteLine(fields: Vector<str>): str
Turns fields into one CSV line, quoting wherever it has to.

The example for this item is shown with the ones above: encoding-csv-Table.salam