Refactor lua helper script to separate file
And write a few tests for it
This commit is contained in:
235
processor/luahelper.lua
Normal file
235
processor/luahelper.lua
Normal file
@@ -0,0 +1,235 @@
|
||||
-- Custom Lua helpers for math operations
|
||||
function min(a, b) return math.min(a, b) end
|
||||
|
||||
function max(a, b) return math.max(a, b) end
|
||||
|
||||
function round(x, n)
|
||||
if n == nil then n = 0 end
|
||||
return math.floor(x * 10 ^ n + 0.5) / 10 ^ n
|
||||
end
|
||||
|
||||
function floor(x) return math.floor(x) end
|
||||
|
||||
function ceil(x) return math.ceil(x) end
|
||||
|
||||
function upper(s) return string.upper(s) end
|
||||
|
||||
function lower(s) return string.lower(s) end
|
||||
|
||||
function format(s, ...) return string.format(s, ...) end
|
||||
|
||||
function trim(s) return string.gsub(s, "^%s*(.-)%s*$", "%1") end
|
||||
|
||||
-- String split helper
|
||||
function strsplit(inputstr, sep)
|
||||
if sep == nil then sep = "%s" end
|
||||
local t = {}
|
||||
for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do
|
||||
table.insert(t, str)
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
---@param table table
|
||||
---@param depth number?
|
||||
function dump(table, depth)
|
||||
if depth == nil then depth = 0 end
|
||||
if depth > 200 then
|
||||
print("Error: Depth > 200 in dump()")
|
||||
return
|
||||
end
|
||||
for k, v in pairs(table) do
|
||||
if type(v) == "table" then
|
||||
print(string.rep(" ", depth) .. k .. ":")
|
||||
dump(v, depth + 1)
|
||||
else
|
||||
print(string.rep(" ", depth) .. k .. ": ", v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Parses CSV text into rows and fields using a minimal RFC 4180 state machine.
|
||||
---
|
||||
--- Requirements/assumptions:
|
||||
--- - Input is a single string containing the entire CSV content.
|
||||
--- - Field separators are specified by delimiter parameter (default: comma).
|
||||
--- - Newlines between rows may be "\n" or "\r\n". "\r\n" is treated as one line break.
|
||||
--- - Fields may be quoted with double quotes (").
|
||||
--- - Inside quoted fields, doubled quotes ("") represent a literal quote character.
|
||||
--- - No backslash escaping is supported (not part of RFC 4180).
|
||||
--- - Newlines inside quoted fields are preserved as part of the field.
|
||||
--- - 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.
|
||||
---
|
||||
--- @param csv string The CSV text to parse.
|
||||
--- @param delimiter string? The field delimiter (default: ",").
|
||||
--- @param hasHeaders boolean? If true, first row is treated as headers and rows can be accessed by header name (default: false).
|
||||
--- @return table A table (array) of rows; each row is a table with numeric indices and optionally header-named keys.
|
||||
function fromCSV(csv, delimiter, hasHeaders)
|
||||
if delimiter == nil then delimiter = "," end
|
||||
if hasHeaders == nil then hasHeaders = false end
|
||||
local allRows = {}
|
||||
local fields = {}
|
||||
local field = {}
|
||||
|
||||
local STATE_DEFAULT = 1
|
||||
local STATE_IN_QUOTES = 2
|
||||
local STATE_QUOTE_IN_QUOTES = 3
|
||||
local state = STATE_DEFAULT
|
||||
|
||||
local i = 1
|
||||
local len = #csv
|
||||
|
||||
while i <= len do
|
||||
local c = csv:sub(i, i)
|
||||
|
||||
if state == STATE_DEFAULT then
|
||||
if c == '"' then
|
||||
state = STATE_IN_QUOTES
|
||||
i = i + 1
|
||||
elseif c == delimiter then
|
||||
table.insert(fields, table.concat(field))
|
||||
field = {}
|
||||
i = i + 1
|
||||
elseif c == "\r" or c == "\n" then
|
||||
table.insert(fields, table.concat(field))
|
||||
field = {}
|
||||
table.insert(allRows, fields)
|
||||
fields = {}
|
||||
if c == "\r" and i < len and csv:sub(i + 1, i + 1) == "\n" then
|
||||
i = i + 2
|
||||
else
|
||||
i = i + 1
|
||||
end
|
||||
else
|
||||
table.insert(field, c)
|
||||
i = i + 1
|
||||
end
|
||||
elseif state == STATE_IN_QUOTES then
|
||||
if c == '"' then
|
||||
state = STATE_QUOTE_IN_QUOTES
|
||||
i = i + 1
|
||||
else
|
||||
table.insert(field, c)
|
||||
i = i + 1
|
||||
end
|
||||
else -- STATE_QUOTE_IN_QUOTES
|
||||
if c == '"' then
|
||||
table.insert(field, '"')
|
||||
state = STATE_IN_QUOTES
|
||||
i = i + 1
|
||||
elseif c == delimiter then
|
||||
table.insert(fields, table.concat(field))
|
||||
field = {}
|
||||
state = STATE_DEFAULT
|
||||
i = i + 1
|
||||
elseif c == "\r" or c == "\n" then
|
||||
table.insert(fields, table.concat(field))
|
||||
field = {}
|
||||
table.insert(allRows, fields)
|
||||
fields = {}
|
||||
state = STATE_DEFAULT
|
||||
if c == "\r" and i < len and csv:sub(i + 1, i + 1) == "\n" then
|
||||
i = i + 2
|
||||
else
|
||||
i = i + 1
|
||||
end
|
||||
else
|
||||
state = STATE_DEFAULT
|
||||
-- Don't increment i, reprocess character in DEFAULT state
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #field > 0 or #fields > 0 then
|
||||
table.insert(fields, table.concat(field))
|
||||
table.insert(allRows, fields)
|
||||
end
|
||||
|
||||
if hasHeaders and #allRows > 0 then
|
||||
local headers = allRows[1]
|
||||
local rows = {}
|
||||
for i = 2, #allRows do
|
||||
local row = {}
|
||||
local dataRow = allRows[i]
|
||||
for j = 1, #dataRow do
|
||||
row[j] = dataRow[j]
|
||||
if headers[j] ~= nil and headers[j] ~= "" then row[headers[j]] = dataRow[j] end
|
||||
end
|
||||
table.insert(rows, row)
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
return allRows
|
||||
end
|
||||
|
||||
--- Converts a table of rows back to CSV text format (RFC 4180 compliant).
|
||||
---
|
||||
--- Requirements:
|
||||
--- - Input is a table (array) of rows, where each row is a table (array) of field values.
|
||||
--- - Field values are converted to strings using tostring().
|
||||
--- - 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.
|
||||
---
|
||||
--- @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
|
||||
local rowStrings = {}
|
||||
|
||||
for _, row in ipairs(rows) do
|
||||
local fieldStrings = {}
|
||||
|
||||
for _, field in ipairs(row) do
|
||||
local fieldStr = tostring(field)
|
||||
local needsQuoting = false
|
||||
|
||||
if
|
||||
fieldStr:find(delimiter)
|
||||
or fieldStr:find("\n")
|
||||
or fieldStr:find("\r")
|
||||
or fieldStr:find('"')
|
||||
then
|
||||
needsQuoting = true
|
||||
end
|
||||
|
||||
if needsQuoting then
|
||||
fieldStr = fieldStr:gsub('"', '""')
|
||||
fieldStr = '"' .. fieldStr .. '"'
|
||||
end
|
||||
|
||||
table.insert(fieldStrings, fieldStr)
|
||||
end
|
||||
|
||||
table.insert(rowStrings, table.concat(fieldStrings, delimiter))
|
||||
end
|
||||
|
||||
return table.concat(rowStrings, "\n")
|
||||
end
|
||||
|
||||
-- String to number conversion helper
|
||||
function num(str) return tonumber(str) or 0 end
|
||||
|
||||
-- Number to string conversion
|
||||
function str(num) return tostring(num) end
|
||||
|
||||
-- Check if string is numeric
|
||||
function is_number(str) return tonumber(str) ~= nil end
|
||||
|
||||
function isArray(t)
|
||||
if type(t) ~= "table" then return false end
|
||||
local max = 0
|
||||
local count = 0
|
||||
for k, _ in pairs(t) do
|
||||
if type(k) ~= "number" or k < 1 or math.floor(k) ~= k then return false end
|
||||
max = math.max(max, k)
|
||||
count = count + 1
|
||||
end
|
||||
return max == count
|
||||
end
|
||||
|
||||
modified = false
|
||||
Reference in New Issue
Block a user