--- 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