How do I read and write CSV in EK9?
← JSON and Data Processing · Ref: Q1364
CSV is a first-class built-in type in EK9 - a full RFC 4180 reader and writer - so you do not reach for split(',') (which breaks the moment a field is quoted) or an external library.
PARSING RETURNS A RESULT
CSV().parse(text) returns a Result of (CSV, String). A malformed document (for example an unterminated quoted field) becomes an error carrying its reason, and - as with Optional - the compiler REQUIRES you to check isOk() before you call ok():
result <- CSV().parse(text) if result.isOk() table <- result.ok()
The separator is configurable via a constructor argument, so CSV(';') or CSV('\t') read semicolon- and tab-separated data.
READING ROWS AND CELLS
A CSV holds a header row and the data rows. Read rows total-style, exactly like List - getOrDefault(index, default) never goes out of range:
table.rows() how many data rows table.header() the header CSVRow row <- table.getOrDefault(0, CSVRow())
A CSVRow is more than a List of String: a data row is header-aware, so a cell can be read by column NAME as well as by position:
row.getOrDefault(1, "?") by position row.getOrDefault("weight", "?") by column name
GETTING A REAL TYPE OUT OF A CELL
Cells are text. To get a real type, hand the cell to that type's constructor, which parses it and returns unset if it is not that type - no throwing:
weight <- BigDecimal(row.getOrDefault("weight", "0")) number <- Integer(row.getOrDefault("number", "0"))
This is uniform for every type, including your own.
ITERATING (AND STREAMS)
A CSV iterates its data rows, so a for-in loop - or a full stream pipeline (filter, sort, head ...) - works directly:
for row in table stdout.println(row.getOrDefault("name", "?"))
WRITING
Build a CSV row-by-row and render it back to RFC 4180 text with $ (fields that contain the separator, a quote or a newline are quoted for you):
out <- CSV() head <- CSVRow() head += "id" head += "label" built <- out.header(head) row <- CSVRow() row += "1" row += "has, comma" built += row csvText <- $built
COMBINING
Stacking two CSVs vertically (+, :~:, +=) is guarded by header alignment - if the two headers are not the same columns the result is unset, so you cannot silently weld mismatched data together.
See Q188 for JSON as a first-class type. See Q23 for basic types. See Q129 for what happens when a key or index is absent (getOrDefault). Use 'ek9 -h CSV' and 'ek9 -h CSVRow' for the full API.
Example
defines module qa.csv.readwrite defines program CsvReadWrite() stdout <- Stdout() //Read: a quoted field keeps its comma; parse returns a Result. result <- CSV().parse("name,weight\nHydrogen,1.008\nHelium,\"4.0026, approx\"") if result.isOk() table <- result.ok() stdout.println(`rows: ${table.rows()}`) //Read a data row and a cell by column name; convert with a constructor. helium <- table.getOrDefault(1, CSVRow()) note <- helium.getOrDefault("weight", "?") stdout.println(`helium weight cell: ${note}`) firstWeight <- BigDecimal(table.getOrDefault(0, CSVRow()).getOrDefault("weight", "0")) stdout.println(`hydrogen weight typed: ${firstWeight}`) //Iterate the data rows. for row in table stdout.println(row.getOrDefault("name", "?")) //Write: build a CSV row-by-row and render to RFC 4180 text. out <- CSV() head <- CSVRow() head += "id" head += "label" built <- out.header(head) dataRow <- CSVRow() dataRow += "1" dataRow += "has, comma" built += dataRow stdout.print($built)
Common mistakes
E08030 — CSV.parse returns a Result of (CSV, String). Like Optional, the compiler enforces a check before access: ok() may only be called after isOk(), and error() only after isError(). Calling ok() (or error()) unguarded triggers E08030. Note the 'else' of an isOk() check does NOT count as an isError() check - a Result can also be empty - so guard error() with its own 'if result.isError()'. See ek9 -h E08030.
Incorrect:
table <- result.ok()
Correct:
if result.isOk() table <- result.ok()
Other ways to ask this
- Is CSV a built-in type in EK9?
- How do I parse a comma separated values file?
- How do I handle quoted CSV fields that contain commas?
- How do I access a CSV cell by column name?
- How do I write CSV output in EK9?
Coming from another language?
Java: CSV needs an external library (Apache Commons CSV, OpenCSV); split(',') is a common but broken shortcut. Python: csv.reader (positional) / csv.DictReader (by column name); cells are always str. Go: encoding/csv. Rust: the csv crate with serde. EK9: CSV and CSVRow are built in. parse returns Result of (CSV, String) with compiler-enforced isOk() before ok(); rows read by index or column name with getOrDefault; cells typed via the target constructor; writer via $. Full RFC 4180 (quoted fields, doubled quotes, embedded newlines), configurable separator.
Keywords: separator, result, comma, data, values, write, parse, header, read, csv, field, table, getOrDefault, quoted, row, rfc4180, file, column, tsv, separated