Acquis 24 - CSV
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.