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" ,bluethe 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:
ReadAllsplits the text into lines first.WriteLinequotes such a field correctly for other tools, but this reader cuts it in two. FromValuetakes the columns from the keys of the first object, in the order they were added, so a table read withToValueis 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.
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)
endGuard 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.
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)
endWrite 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.
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"))
endEvery 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:
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- 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.Trimif your source pads columns. - Windows line endings (
\r\n) are handled byReadAll. ReadAllskips blank lines, so a table never has empty rows.- A space between a closing quote and the comma ends the row:
"Pen" ,blue,3gives one field. - A line with semicolons is one field, because only commas separate fields.
ReadAllsplits 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| Name | Type | Description |
|---|---|---|
fields | Vector | The row's fields. |
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)
endCSVTable
struct CSVTable| Name | Type | Description |
|---|---|---|
rows | Vector | The 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| Name | Type | Description |
|---|---|---|
header | bool = true | The 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\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)The example for this item is shown with the ones above: encoding-csv-Table.salam
WriteAll
func WriteAll(table: CSVTable): str\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.ValueValue 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.ValueThe example for this item is shown with the ones above: encoding-csv-Table.salam
FromValueWith
func FromValueWith(v: value.Value, opts: CSVOptions): strThe example for this item is shown with the ones above: encoding-csv-Table.salam
FromValue
func FromValue(v: value.Value): strWriteLine 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>The example for this item is shown with the ones above: encoding-csv-Table.salam
WriteLine
func WriteLine(fields: Vector<str>): strThe example for this item is shown with the ones above: encoding-csv-Table.salam