Acquis 24 - CSV

This manual page contains unstable information and its contents may change at any time.

The fromcsv and tocsv functions provide built-in CSV serialization and deserialization, following the same pattern as the existing fromjson and tojson functions.

Parsing CSV

fromcsv takes a CSV string and returns a table of rows. Each row is a table of string values.

local data = fromcsv("name,age,city\nAlice,30,Paris\nBob,25,London")

assert(#data == 3)
assert(data[1][1] == "name")
assert(data[2][1] == "Alice")
assert(data[2][2] == "30")

Headers

When the optional headers argument is true, the first row is treated as column headers and each subsequent row is returned as a table keyed by those headers instead of numeric indices.

local data = fromcsv("name,age,city\nAlice,30,Paris\nBob,25,London", true)

assert(#data == 2)
assert(data[1].name == "Alice")
assert(data[1].age == "30")
assert(data[1].city == "Paris")
assert(data[2].name == "Bob")

Quoted fields

Fields containing commas, newlines, or double quotes can be enclosed in double quotes. A literal double quote inside a quoted field is escaped by doubling it, per RFC 4180.

local data = fromcsv('"hello, world","she said ""hi"""')

assert(data[1][1] == "hello, world")
assert(data[1][2] == 'she said "hi"')

Custom delimiter

An optional third argument specifies the field delimiter. The default is ,.

local data = fromcsv("name\tage\nAlice\t30", false, "\t")

assert(data[1][1] == "name")
assert(data[2][2] == "30")

Serializing CSV

tocsv converts a table of rows to a CSV string. Each row is a sequential table of values.

local csv = tocsv({
    {"name", "age", "city"},
    {"Alice", "30", "Paris"},
    {"Bob", "25", "London"},
})

assert(csv == "name,age,city\nAlice,30,Paris\nBob,25,London\n")

Automatic quoting

Fields that contain the delimiter, newlines, or double quotes are automatically quoted. Double quotes within fields are escaped by doubling them.

local csv = tocsv({{"hello, world", 'say "hi"'}})

assert(csv == '"hello, world","say ""hi"""\n')

Non-string values

Non-string values are converted to their string representation via tostring before serialization. nil values produce an empty field.

local csv = tocsv({{1, true, nil, 3.14}})

assert(csv == "1,true,,3.14\n")

Custom delimiter

An optional second argument specifies the field delimiter. The default is ,.

local csv = tocsv({{"a", "b"}, {"c", "d"}}, "\t")

assert(csv == "a\tb\nc\td\n")

Error handling

Both functions raise errors on malformed input:

local ok, err = catch fromcsv(42)         -- not a string
assert(not ok)

local ok, err = catch fromcsv('"unterminated') -- unclosed quote
assert(not ok)

local ok, err = catch tocsv("not a table") -- not a table
assert(not ok)

Motivation

CSV is one of the most common data interchange formats. Like JSON, it appears frequently enough in practical programming that built-in support eliminates the need for external dependencies.

Consistency with JSON

Lus already provides fromjson and tojson as global functions for the most common serialization format. CSV is the natural complement — it covers the tabular data use case that JSON handles awkwardly.

-- Reading a CSV file is as simple as reading JSON
local data = fromcsv(io.open("data.csv"):read("*a"), true)
for _, row in ipairs(data) do
    print(row.name, row.age)
end

No external dependencies

Without built-in support, CSV parsing requires either pulling in a library or writing a parser manually — both of which are error-prone, especially around edge cases like quoted fields with embedded newlines and escaped quotes. A built-in implementation following RFC 4180 handles these correctly.