diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:42:08 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:42:08 -0300 |
| commit | 7fd555fcaf4304cb0dff3e8f5661565c3618299e (patch) | |
| tree | 61199793c3046d73b97e9822895ede2a33034d41 /cepis/csv.lua | |
| download | lua-7fd555fcaf4304cb0dff3e8f5661565c3618299e.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'cepis/csv.lua')
| -rw-r--r-- | cepis/csv.lua | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/cepis/csv.lua b/cepis/csv.lua new file mode 100644 index 0000000..be05c9a --- /dev/null +++ b/cepis/csv.lua @@ -0,0 +1,109 @@ +--- A minimal RFC 4180 reader, for the batch endpoint. +-- +-- The values the API sends may contain commas — a `logradouro` such as +-- `Rua Um, Dois` — and are then double-quoted with `""` escaping. Splitting on +-- `,` silently misaligns every column after the first such value, which is why +-- this exists instead of a `gmatch`. +-- +-- @module cepis.csv + +local M = {} + +local BOM = "\239\187\191" + +--- Parse CSV text into a list of rows, each a list of field strings. +-- +-- Handles quoted fields, `""` escapes, embedded commas and newlines, both LF +-- and CRLF line endings, and a leading UTF-8 BOM. Blank lines yield no row. +-- Nothing is coerced: every field comes back as a string. +-- +-- Text between a closing quote and the next delimiter is appended to the field +-- rather than starting a new one: malformed input must not silently gain a +-- column and shift every value after it. +-- +-- @tparam string text the response body +-- @treturn table list of rows; each row is a list of strings +function M.parse(text) + if text:sub(1, 3) == BOM then + text = text:sub(4) + end + + local rows = {} + local row = {} + local count = 0 + local pos = 1 + local size = #text + + local function close_row() + if not (count == 1 and row[1] == "") then + rows[#rows + 1] = row + end + row = {} + count = 0 + end + + while pos <= size do + local field + if text:sub(pos, pos) == '"' then + local parts = {} + pos = pos + 1 + while true do + local quote = text:find('"', pos, true) + if not quote then + parts[#parts + 1] = text:sub(pos) + pos = size + 1 + break + end + parts[#parts + 1] = text:sub(pos, quote - 1) + if text:sub(quote + 1, quote + 1) == '"' then + parts[#parts + 1] = '"' + pos = quote + 2 + else + pos = quote + 1 + break + end + end + local trailing = text:find("[,\r\n]", pos) + if trailing ~= pos then + parts[#parts + 1] = text:sub(pos, (trailing or size + 1) - 1) + pos = trailing or size + 1 + end + field = table.concat(parts) + else + local stop = text:find("[,\r\n]", pos) + if stop then + field = text:sub(pos, stop - 1) + pos = stop + else + field = text:sub(pos) + pos = size + 1 + end + end + + count = count + 1 + row[count] = field + + local char = text:sub(pos, pos) + if char == "," then + pos = pos + 1 + if pos > size then + count = count + 1 + row[count] = "" + close_row() + end + elseif char == "\r" or char == "\n" then + if char == "\r" and text:sub(pos + 1, pos + 1) == "\n" then + pos = pos + 2 + else + pos = pos + 1 + end + close_row() + else + close_row() + end + end + + return rows +end + +return M |