Check options passed to csv parser
This commit is contained in:
@@ -53,6 +53,34 @@ end
|
||||
--- @field hasheader boolean? If true, first non-comment row is treated as headers (default: false).
|
||||
--- @field hascomments boolean? If true, lines starting with '#' are skipped (default: false).
|
||||
|
||||
--- @type ParserOptions
|
||||
parserDefaultOptions = { delimiter = ",", hasheader = false, hascomments = false }
|
||||
|
||||
--- Validates options against a set of valid option keys.
|
||||
--- @param options ParserOptions? The options table to validate
|
||||
--- @return boolean #True if options are valid
|
||||
--- @return string? #Error message if invalid, nil if valid
|
||||
function areOptionsValid(options)
|
||||
if options == nil then return true, nil end
|
||||
|
||||
if type(options) ~= "table" then return false, "options must be a table" end
|
||||
|
||||
-- Build valid options list from validOptions table
|
||||
local validOptionsStr = ""
|
||||
for k, _ in pairs(parserDefaultOptions) do
|
||||
validOptionsStr = validOptionsStr .. k .. ", "
|
||||
end
|
||||
|
||||
for k, _ in pairs(options) do
|
||||
if parserDefaultOptions[k] == nil then
|
||||
return false,
|
||||
"unknown option: " .. tostring(k) .. " (valid options: " .. validOptionsStr .. ")"
|
||||
end
|
||||
end
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
--- Parses CSV text into rows and fields using a minimal RFC 4180 state machine.
|
||||
---
|
||||
--- Requirements/assumptions:
|
||||
@@ -66,16 +94,22 @@ end
|
||||
--- - Leading/trailing spaces are preserved; no trimming is performed.
|
||||
--- - Empty fields and empty rows are preserved.
|
||||
--- - The final row is emitted even if the text does not end with a newline.
|
||||
--- - Lines starting with '#' (after optional leading whitespace) are treated as comments and skipped if hasComments is true.
|
||||
--- - Lines starting with '#' (after optional leading whitespace) are treated as comments and skipped if hascomments is true.
|
||||
---
|
||||
--- @param csv string The CSV text to parse.
|
||||
--- @param options ParserOptions? Options for the parser
|
||||
--- @return table A table (array) of rows; each row is a table with numeric indices and optionally header-named keys.
|
||||
--- @return table #A table (array) of rows; each row is a table with numeric indices and optionally header-named keys.
|
||||
--- @return string? #Error message if parsing fails.
|
||||
function fromCSV(csv, options)
|
||||
if options == nil then options = {} end
|
||||
local delimiter = options.delimiter or ","
|
||||
local hasheader = options.hasheader or false
|
||||
local hascomments = options.hascomments or false
|
||||
|
||||
-- Validate options
|
||||
local isValid, err = areOptionsValid(options)
|
||||
if not isValid then return {}, err end
|
||||
|
||||
local delimiter = options.delimiter or parserDefaultOptions.delimiter
|
||||
local hasheader = options.hasheader or parserDefaultOptions.hasheader
|
||||
local hascomments = options.hascomments or parserDefaultOptions.hascomments
|
||||
|
||||
local allRows = {}
|
||||
local fields = {}
|
||||
@@ -186,12 +220,13 @@ function fromCSV(csv, options)
|
||||
row[headerName] = dataRow[j]
|
||||
end
|
||||
end
|
||||
row.Headers = headers
|
||||
table.insert(rows, row)
|
||||
end
|
||||
return rows
|
||||
return rows, nil
|
||||
end
|
||||
|
||||
return allRows
|
||||
return allRows, nil
|
||||
end
|
||||
|
||||
--- Converts a table of rows back to CSV text format (RFC 4180 compliant).
|
||||
@@ -202,14 +237,46 @@ end
|
||||
--- - Fields are quoted if they contain the delimiter, newlines, or double quotes.
|
||||
--- - Double quotes inside quoted fields are doubled ("").
|
||||
--- - Fields are joined with the specified delimiter; rows are joined with newlines.
|
||||
--- - If includeHeaders is true and rows have a Headers field, headers are included as the first row.
|
||||
---
|
||||
--- @param rows table Array of rows, where each row is an array of field values.
|
||||
--- @param delimiter string? The field delimiter (default: ",").
|
||||
--- @return string CSV-formatted text.
|
||||
function toCSV(rows, delimiter)
|
||||
if delimiter == nil then delimiter = "," end
|
||||
--- @param options ParserOptions? Options for the parser
|
||||
--- @return string? #CSV-formatted text, error string?
|
||||
--- @return string? #Error message if conversion fails.
|
||||
function toCSV(rows, options)
|
||||
if options == nil then options = {} end
|
||||
|
||||
-- Validate options
|
||||
local isValid, err = areOptionsValid(options)
|
||||
if not isValid then return nil, err end
|
||||
|
||||
local delimiter = options.delimiter or parserDefaultOptions.delimiter
|
||||
local includeHeaders = options.hasheader or parserDefaultOptions.hasheader
|
||||
local rowStrings = {}
|
||||
|
||||
-- Include headers row if requested and available
|
||||
if includeHeaders and #rows > 0 and rows[1].Headers ~= nil then
|
||||
local headerStrings = {}
|
||||
for _, header in ipairs(rows[1].Headers) do
|
||||
local headerStr = tostring(header)
|
||||
local needsQuoting = false
|
||||
if
|
||||
headerStr:find(delimiter)
|
||||
or headerStr:find("\n")
|
||||
or headerStr:find("\r")
|
||||
or headerStr:find('"')
|
||||
then
|
||||
needsQuoting = true
|
||||
end
|
||||
if needsQuoting then
|
||||
headerStr = headerStr:gsub('"', '""')
|
||||
headerStr = '"' .. headerStr .. '"'
|
||||
end
|
||||
table.insert(headerStrings, headerStr)
|
||||
end
|
||||
table.insert(rowStrings, table.concat(headerStrings, delimiter))
|
||||
end
|
||||
|
||||
for _, row in ipairs(rows) do
|
||||
local fieldStrings = {}
|
||||
|
||||
@@ -237,7 +304,7 @@ function toCSV(rows, delimiter)
|
||||
table.insert(rowStrings, table.concat(fieldStrings, delimiter))
|
||||
end
|
||||
|
||||
return table.concat(rowStrings, "\n")
|
||||
return table.concat(rowStrings, "\n"), nil
|
||||
end
|
||||
|
||||
-- String to number conversion helper
|
||||
|
||||
Reference in New Issue
Block a user