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