Check options passed to csv parser

This commit is contained in:
2025-11-15 16:06:31 +01:00
parent aec0f9f171
commit bf23894188
2 changed files with 224 additions and 40 deletions

View File

@@ -15,10 +15,29 @@ local function test(name, fn)
end end
end end
-- Test fromCSV option validation
test("fromCSV invalid option", function()
local csv = "a,b,c\n1,2,3"
local rows, err = fromCSV(csv, { invalidOption = true })
assert(rows ~= nil and #rows == 0, "Should return empty table on error")
assert(err ~= nil, "Should return error message")
assert(string.find(err, "unknown option"), "Error should mention unknown option")
end)
-- Test toCSV error handling
test("toCSV invalid delimiter", function()
local rows = { { "a", "b", "c" } }
local csv, err = toCSV(rows, { delimiter = 123 })
-- toCSV converts delimiter to string, so 123 becomes "123"
assert(csv == "a123b123c", "Should convert delimiter to string")
assert(err == nil, "Should not return error")
end)
-- Test fromCSV basic parsing -- Test fromCSV basic parsing
test("fromCSV basic", function() test("fromCSV basic", function()
local csv = "a,b,c\n1,2,3\n4,5,6" local csv = "a,b,c\n1,2,3\n4,5,6"
local rows = fromCSV(csv) local rows, err = fromCSV(csv)
if err then error("fromCSV error: " .. err) end
assert(#rows == 3, "Should have 3 rows") assert(#rows == 3, "Should have 3 rows")
assert(rows[1][1] == "a", "First row first field should be 'a'") assert(rows[1][1] == "a", "First row first field should be 'a'")
assert(rows[2][2] == "2", "Second row second field should be '2'") assert(rows[2][2] == "2", "Second row second field should be '2'")
@@ -27,7 +46,8 @@ end)
-- Test fromCSV with headers -- Test fromCSV with headers
test("fromCSV with headers", function() test("fromCSV with headers", function()
local csv = "foo,bar,baz\n1,2,3\n4,5,6" local csv = "foo,bar,baz\n1,2,3\n4,5,6"
local rows = fromCSV(csv, { hasHeaders = true }) local rows, err = fromCSV(csv, { hasheader = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 data rows") assert(#rows == 2, "Should have 2 data rows")
assert(rows[1][1] == "1", "First row first field should be '1'") assert(rows[1][1] == "1", "First row first field should be '1'")
assert(rows[1].foo == "1", "First row foo should be '1'") assert(rows[1].foo == "1", "First row foo should be '1'")
@@ -38,7 +58,8 @@ end)
-- Test fromCSV with custom delimiter -- Test fromCSV with custom delimiter
test("fromCSV with tab delimiter", function() test("fromCSV with tab delimiter", function()
local csv = "a\tb\tc\n1\t2\t3" local csv = "a\tb\tc\n1\t2\t3"
local rows = fromCSV(csv, { delimiter = "\t" }) local rows, err = fromCSV(csv, { delimiter = "\t" })
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 rows") assert(#rows == 2, "Should have 2 rows")
assert(rows[1][1] == "a", "First row first field should be 'a'") assert(rows[1][1] == "a", "First row first field should be 'a'")
assert(rows[2][2] == "2", "Second row second field should be '2'") assert(rows[2][2] == "2", "Second row second field should be '2'")
@@ -47,7 +68,8 @@ end)
-- Test fromCSV with quoted fields -- Test fromCSV with quoted fields
test("fromCSV with quoted fields", function() test("fromCSV with quoted fields", function()
local csv = '"hello,world","test"\n"foo","bar"' local csv = '"hello,world","test"\n"foo","bar"'
local rows = fromCSV(csv) local rows, err = fromCSV(csv)
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 rows") assert(#rows == 2, "Should have 2 rows")
assert(rows[1][1] == "hello,world", "Quoted field with comma should be preserved") assert(rows[1][1] == "hello,world", "Quoted field with comma should be preserved")
assert(rows[1][2] == "test", "Second field should be 'test'") assert(rows[1][2] == "test", "Second field should be 'test'")
@@ -56,37 +78,44 @@ end)
-- Test toCSV basic -- Test toCSV basic
test("toCSV basic", function() test("toCSV basic", function()
local rows = { { "a", "b", "c" }, { "1", "2", "3" } } local rows = { { "a", "b", "c" }, { "1", "2", "3" } }
local csv = toCSV(rows) local csv, err = toCSV(rows)
if err then error("toCSV error: " .. err) end
assert(csv == "a,b,c\n1,2,3", "CSV output should match expected") assert(csv == "a,b,c\n1,2,3", "CSV output should match expected")
end) end)
-- Test toCSV with custom delimiter -- Test toCSV with custom delimiter
test("toCSV with tab delimiter", function() test("toCSV with tab delimiter", function()
local rows = { { "a", "b", "c" }, { "1", "2", "3" } } local rows = { { "a", "b", "c" }, { "1", "2", "3" } }
local csv = toCSV(rows, "\t") local csv, err = toCSV(rows, { delimiter = "\t" })
if err then error("toCSV error: " .. err) end
assert(csv == "a\tb\tc\n1\t2\t3", "TSV output should match expected") assert(csv == "a\tb\tc\n1\t2\t3", "TSV output should match expected")
end) end)
-- Test toCSV with fields needing quoting -- Test toCSV with fields needing quoting
test("toCSV with quoted fields", function() test("toCSV with quoted fields", function()
local rows = { { "hello,world", "test" }, { "foo", "bar" } } local rows = { { "hello,world", "test" }, { "foo", "bar" } }
local csv = toCSV(rows) local csv, err = toCSV(rows)
if err then error("toCSV error: " .. err) end
assert(csv == '"hello,world",test\nfoo,bar', "Fields with commas should be quoted") assert(csv == '"hello,world",test\nfoo,bar', "Fields with commas should be quoted")
end) end)
-- Test round trip -- Test round trip
test("fromCSV toCSV round trip", function() test("fromCSV toCSV round trip", function()
local original = "a,b,c\n1,2,3\n4,5,6" local original = "a,b,c\n1,2,3\n4,5,6"
local rows = fromCSV(original) local rows, err = fromCSV(original)
local csv = toCSV(rows) if err then error("fromCSV error: " .. err) end
local csv, err = toCSV(rows)
if err then error("toCSV error: " .. err) end
assert(csv == original, "Round trip should preserve original") assert(csv == original, "Round trip should preserve original")
end) end)
-- Test round trip with headers -- Test round trip with headers
test("fromCSV toCSV round trip with headers", function() test("fromCSV toCSV round trip with headers", function()
local original = "foo,bar,baz\n1,2,3\n4,5,6" local original = "foo,bar,baz\n1,2,3\n4,5,6"
local rows = fromCSV(original, { hasHeaders = true }) local rows, err = fromCSV(original, { hasheader = true })
local csv = toCSV(rows) if err then error("fromCSV error: " .. err) end
local csv, err = toCSV(rows)
if err then error("toCSV error: " .. err) end
local expected = "1,2,3\n4,5,6" local expected = "1,2,3\n4,5,6"
assert(csv == expected, "Round trip with headers should preserve data rows") assert(csv == expected, "Round trip with headers should preserve data rows")
end) end)
@@ -94,7 +123,8 @@ end)
-- Test fromCSV with comments -- Test fromCSV with comments
test("fromCSV with comments", function() test("fromCSV with comments", function()
local csv = "# This is a comment\nfoo,bar,baz\n1,2,3\n# Another comment\n4,5,6" local csv = "# This is a comment\nfoo,bar,baz\n1,2,3\n# Another comment\n4,5,6"
local rows = fromCSV(csv, { hasComments = true }) local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 3, "Should have 3 rows (comments filtered, header + 2 data rows)") assert(#rows == 3, "Should have 3 rows (comments filtered, header + 2 data rows)")
assert(rows[1][1] == "foo", "First row should be header row") assert(rows[1][1] == "foo", "First row should be header row")
assert(rows[2][1] == "1", "Second row first field should be '1'") assert(rows[2][1] == "1", "Second row first field should be '1'")
@@ -104,7 +134,8 @@ end)
-- Test fromCSV with comments and headers -- Test fromCSV with comments and headers
test("fromCSV with comments and headers", function() test("fromCSV with comments and headers", function()
local csv = "#mercenary_profiles\nId,Name,Value\n1,Test,100\n# End of data\n2,Test2,200" local csv = "#mercenary_profiles\nId,Name,Value\n1,Test,100\n# End of data\n2,Test2,200"
local rows = fromCSV(csv, { hasHeaders = true, hasComments = true }) local rows, err = fromCSV(csv, { hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 data rows") assert(#rows == 2, "Should have 2 data rows")
assert(rows[1].Id == "1", "First row Id should be '1'") assert(rows[1].Id == "1", "First row Id should be '1'")
assert(rows[1].Name == "Test", "First row Name should be 'Test'") assert(rows[1].Name == "Test", "First row Name should be 'Test'")
@@ -115,7 +146,8 @@ end)
-- Test fromCSV with comments disabled -- Test fromCSV with comments disabled
test("fromCSV without comments", function() test("fromCSV without comments", function()
local csv = "# This should not be filtered\nfoo,bar\n1,2" local csv = "# This should not be filtered\nfoo,bar\n1,2"
local rows = fromCSV(csv, { hasComments = false }) local rows, err = fromCSV(csv, { hascomments = false })
if err then error("fromCSV error: " .. err) end
assert(#rows == 3, "Should have 3 rows (including comment)") assert(#rows == 3, "Should have 3 rows (including comment)")
assert(rows[1][1] == "# This should not be filtered", "Comment line should be preserved") assert(rows[1][1] == "# This should not be filtered", "Comment line should be preserved")
end) end)
@@ -123,7 +155,8 @@ end)
-- Test fromCSV with comment at start -- Test fromCSV with comment at start
test("fromCSV comment at start", function() test("fromCSV comment at start", function()
local csv = "# Header comment\nId,Name\n1,Test" local csv = "# Header comment\nId,Name\n1,Test"
local rows = fromCSV(csv, { hasComments = true }) local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 rows (comment filtered)") assert(#rows == 2, "Should have 2 rows (comment filtered)")
assert(rows[1][1] == "Id", "First row should be header") assert(rows[1][1] == "Id", "First row should be header")
end) end)
@@ -131,7 +164,8 @@ end)
-- Test fromCSV with comment with leading whitespace -- Test fromCSV with comment with leading whitespace
test("fromCSV comment with whitespace", function() test("fromCSV comment with whitespace", function()
local csv = " # Comment with spaces\nId,Name\n1,Test" local csv = " # Comment with spaces\nId,Name\n1,Test"
local rows = fromCSV(csv, { hasComments = true }) local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 rows (comment with spaces filtered)") assert(#rows == 2, "Should have 2 rows (comment with spaces filtered)")
assert(rows[1][1] == "Id", "First row should be header") assert(rows[1][1] == "Id", "First row should be header")
end) end)
@@ -139,7 +173,8 @@ end)
-- Test fromCSV with comment with tabs -- Test fromCSV with comment with tabs
test("fromCSV comment with tabs", function() test("fromCSV comment with tabs", function()
local csv = "\t# Comment with tab\nId,Name\n1,Test" local csv = "\t# Comment with tab\nId,Name\n1,Test"
local rows = fromCSV(csv, { hasComments = true }) local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 rows (comment with tab filtered)") assert(#rows == 2, "Should have 2 rows (comment with tab filtered)")
assert(rows[1][1] == "Id", "First row should be header") assert(rows[1][1] == "Id", "First row should be header")
end) end)
@@ -147,7 +182,8 @@ end)
-- Test fromCSV with multiple consecutive comments -- Test fromCSV with multiple consecutive comments
test("fromCSV multiple consecutive comments", function() test("fromCSV multiple consecutive comments", function()
local csv = "# First comment\n# Second comment\n# Third comment\nId,Name\n1,Test" local csv = "# First comment\n# Second comment\n# Third comment\nId,Name\n1,Test"
local rows = fromCSV(csv, { hasComments = true }) local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 rows (all comments filtered)") assert(#rows == 2, "Should have 2 rows (all comments filtered)")
assert(rows[1][1] == "Id", "First row should be header") assert(rows[1][1] == "Id", "First row should be header")
end) end)
@@ -155,7 +191,8 @@ end)
-- Test fromCSV with comment in middle of data -- Test fromCSV with comment in middle of data
test("fromCSV comment in middle", function() test("fromCSV comment in middle", function()
local csv = "Id,Name\n1,Test\n# Middle comment\n2,Test2" local csv = "Id,Name\n1,Test\n# Middle comment\n2,Test2"
local rows = fromCSV(csv, { hasComments = true }) local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 3, "Should have 3 rows (comment filtered)") assert(#rows == 3, "Should have 3 rows (comment filtered)")
assert(rows[1][1] == "Id", "First row should be header") assert(rows[1][1] == "Id", "First row should be header")
assert(rows[2][1] == "1", "Second row should be first data") assert(rows[2][1] == "1", "Second row should be first data")
@@ -165,7 +202,8 @@ end)
-- Test fromCSV with comment at end -- Test fromCSV with comment at end
test("fromCSV comment at end", function() test("fromCSV comment at end", function()
local csv = "Id,Name\n1,Test\n# End comment" local csv = "Id,Name\n1,Test\n# End comment"
local rows = fromCSV(csv, { hasComments = true }) local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 rows (end comment filtered)") assert(#rows == 2, "Should have 2 rows (end comment filtered)")
assert(rows[1][1] == "Id", "First row should be header") assert(rows[1][1] == "Id", "First row should be header")
assert(rows[2][1] == "1", "Second row should be data") assert(rows[2][1] == "1", "Second row should be data")
@@ -174,7 +212,8 @@ end)
-- Test fromCSV with empty comment line -- Test fromCSV with empty comment line
test("fromCSV empty comment", function() test("fromCSV empty comment", function()
local csv = "#\nId,Name\n1,Test" local csv = "#\nId,Name\n1,Test"
local rows = fromCSV(csv, { hasComments = true }) local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 rows (empty comment filtered)") assert(#rows == 2, "Should have 2 rows (empty comment filtered)")
assert(rows[1][1] == "Id", "First row should be header") assert(rows[1][1] == "Id", "First row should be header")
end) end)
@@ -182,7 +221,8 @@ end)
-- Test fromCSV with comment and headers -- Test fromCSV with comment and headers
test("fromCSV comment with headers enabled", function() test("fromCSV comment with headers enabled", function()
local csv = "#mercenary_profiles\nId,Name,Value\n1,Test,100\n2,Test2,200" local csv = "#mercenary_profiles\nId,Name,Value\n1,Test,100\n2,Test2,200"
local rows = fromCSV(csv, { hasHeaders = true, hasComments = true }) local rows, err = fromCSV(csv, { hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 data rows") assert(#rows == 2, "Should have 2 data rows")
assert(rows[1].Id == "1", "First row Id should be '1'") assert(rows[1].Id == "1", "First row Id should be '1'")
assert(rows[1].Name == "Test", "First row Name should be 'Test'") assert(rows[1].Name == "Test", "First row Name should be 'Test'")
@@ -192,7 +232,8 @@ end)
-- Test fromCSV with comment and TSV delimiter -- Test fromCSV with comment and TSV delimiter
test("fromCSV comment with tab delimiter", function() test("fromCSV comment with tab delimiter", function()
local csv = "# Comment\nId\tName\n1\tTest" local csv = "# Comment\nId\tName\n1\tTest"
local rows = fromCSV(csv, { delimiter = "\t", hasComments = true }) local rows, err = fromCSV(csv, { delimiter = "\t", hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 rows") assert(#rows == 2, "Should have 2 rows")
assert(rows[1][1] == "Id", "First row should be header") assert(rows[1][1] == "Id", "First row should be header")
assert(rows[2][1] == "1", "Second row first field should be '1'") assert(rows[2][1] == "1", "Second row first field should be '1'")
@@ -201,7 +242,8 @@ end)
-- Test fromCSV with comment and headers and TSV -- Test fromCSV with comment and headers and TSV
test("fromCSV comment with headers and TSV", function() test("fromCSV comment with headers and TSV", function()
local csv = "#mercenary_profiles\nId\tName\tValue\n1\tTest\t100" local csv = "#mercenary_profiles\nId\tName\tValue\n1\tTest\t100"
local rows = fromCSV(csv, { delimiter = "\t", hasHeaders = true, hasComments = true }) local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 1, "Should have 1 data row") assert(#rows == 1, "Should have 1 data row")
assert(rows[1].Id == "1", "Row Id should be '1'") assert(rows[1].Id == "1", "Row Id should be '1'")
assert(rows[1].Name == "Test", "Row Name should be 'Test'") assert(rows[1].Name == "Test", "Row Name should be 'Test'")
@@ -211,7 +253,8 @@ end)
-- Test fromCSV with data field starting with # (not a comment) -- Test fromCSV with data field starting with # (not a comment)
test("fromCSV data field starting with hash", function() test("fromCSV data field starting with hash", function()
local csv = "Id,Name\n1,#NotAComment\n2,Test" local csv = "Id,Name\n1,#NotAComment\n2,Test"
local rows = fromCSV(csv, { hasComments = true }) local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 3, "Should have 3 rows (data with # not filtered)") assert(#rows == 3, "Should have 3 rows (data with # not filtered)")
assert(rows[1][1] == "Id", "First row should be header") assert(rows[1][1] == "Id", "First row should be header")
assert(rows[2][2] == "#NotAComment", "Second row should have #NotAComment as data") assert(rows[2][2] == "#NotAComment", "Second row should have #NotAComment as data")
@@ -220,7 +263,8 @@ end)
-- Test fromCSV with quoted field starting with # -- Test fromCSV with quoted field starting with #
test("fromCSV quoted field with hash", function() test("fromCSV quoted field with hash", function()
local csv = 'Id,Name\n1,"#NotAComment"\n2,Test' local csv = 'Id,Name\n1,"#NotAComment"\n2,Test'
local rows = fromCSV(csv, { hasComments = true }) local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 3, "Should have 3 rows (quoted # not filtered)") assert(#rows == 3, "Should have 3 rows (quoted # not filtered)")
assert(rows[2][2] == "#NotAComment", "Quoted field with # should be preserved") assert(rows[2][2] == "#NotAComment", "Quoted field with # should be preserved")
end) end)
@@ -228,7 +272,8 @@ end)
-- Test fromCSV with comment after quoted field -- Test fromCSV with comment after quoted field
test("fromCSV comment after quoted field", function() test("fromCSV comment after quoted field", function()
local csv = 'Id,Name\n1,"Test"\n# This is a comment\n2,Test2' local csv = 'Id,Name\n1,"Test"\n# This is a comment\n2,Test2'
local rows = fromCSV(csv, { hasComments = true }) local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 3, "Should have 3 rows (comment filtered)") assert(#rows == 3, "Should have 3 rows (comment filtered)")
assert(rows[2][2] == "Test", "Quoted field should be preserved") assert(rows[2][2] == "Test", "Quoted field should be preserved")
assert(rows[3][1] == "2", "Third row should be second data row") assert(rows[3][1] == "2", "Third row should be second data row")
@@ -362,13 +407,85 @@ test("isArray function", function()
assert(isArray(123) == false, "isArray should return false for number") assert(isArray(123) == false, "isArray should return false for number")
end) end)
test("toCSV assigns header keys correctly", function() test("fromCSV assigns header keys correctly", function()
local teststr = [[ local teststr = [[
#mercenary_profiles #mercenary_profiles
Id ModifyStartCost ModifyStep ModifyLevelLimit Health ResistSheet WoundSlots MeleeDamage MeleeAccuracy RangeAccuracy ReceiveAmputationChance ReceiveWoundChanceMult AttackWoundChanceMult Dodge Los StarvationLimit PainThresholdLimit PainThresholdRegen TalentPerkId ActorId SkinIndex HairType HairColorHex VoiceBank Immunity CreatureClass Id ModifyStartCost ModifyStep ModifyLevelLimit Health ResistSheet WoundSlots MeleeDamage MeleeAccuracy RangeAccuracy ReceiveAmputationChance ReceiveWoundChanceMult AttackWoundChanceMult Dodge Los StarvationLimit PainThresholdLimit PainThresholdRegen TalentPerkId ActorId SkinIndex HairType HairColorHex VoiceBank Immunity CreatureClass
john_hawkwood_boss 20 0.1 140 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 8 16 crit 1.60 critchance 0.05 0.5 0.5 0.03 0.5 1.2 0.3 8 2200 16 2 talent_the_man_who_sold_the_world human_male 0 hair1 #633D08 player Human john_hawkwood_boss 20 0.1 140 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 8 16 crit 1.60 critchance 0.05 0.5 0.5 0.03 0.5 1.2 0.3 8 2200 16 2 talent_the_man_who_sold_the_world human_male 0 hair1 #633D08 player Human
francis_reid_daly 20 0.1 130 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 7 14 crit 1.70 critchance 0.05 0.5 0.4 0.04 0.9 1 0.3 8 2000 10 1 talent_weapon_durability human_male 0 player Human francis_reid_daly 20 0.1 130 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 7 14 crit 1.70 critchance 0.05 0.5 0.4 0.04 0.9 1 0.3 8 2000 10 1 talent_weapon_durability human_male 0 player Human
]] ]]
local rows, err = fromCSV(teststr, { delimiter = "\t", hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 2, "Should have 2 data rows")
-- Test first row
assert(rows[1].Id == "john_hawkwood_boss", "First row Id should be 'john_hawkwood_boss'")
assert(rows[1].ModifyStartCost == "20", "First row ModifyStartCost should be '20'")
assert(rows[1].ModifyStep == "0.1", "First row ModifyStep should be '0.1'")
assert(rows[1].Health == "140", "First row Health should be '140'")
assert(rows[1].ActorId == "human_male", "First row ActorId should be 'human_male'")
assert(rows[1].HairColorHex == "#633D08", "First row HairColorHex should be '#633D08'")
-- Test second row
assert(rows[2].Id == "francis_reid_daly", "Second row Id should be 'francis_reid_daly'")
assert(rows[2].ModifyStartCost == "20", "Second row ModifyStartCost should be '20'")
assert(rows[2].ModifyStep == "0.1", "Second row ModifyStep should be '0.1'")
assert(rows[2].Health == "130", "Second row Health should be '130'")
assert(rows[2].ActorId == "human_male", "Second row ActorId should be 'human_male'")
-- Test that numeric indices still work
assert(rows[1][1] == "john_hawkwood_boss", "First row first field by index should work")
assert(rows[1][2] == "20", "First row second field by index should work")
-- Debug: Print first row keys to verify headers are assigned
print("DEBUG: First row keys:")
for k, v in pairs(rows[1]) do
if type(k) == "string" then print(" " .. k .. " = " .. tostring(v)) end
end
end)
test("fromCSV debug header assignment", function()
local csv = "Id Name Value\n1 Test 100\n2 Test2 200"
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true })
if err then error("fromCSV error: " .. err) end
print("DEBUG: Number of rows: " .. #rows)
if #rows > 0 then
print("DEBUG: First row Id = " .. tostring(rows[1].Id))
print("DEBUG: First row Name = " .. tostring(rows[1].Name))
print("DEBUG: First row Value = " .. tostring(rows[1].Value))
print("DEBUG: First row keys:")
for k, v in pairs(rows[1]) do
print(" " .. tostring(k) .. " (" .. type(k) .. ") = " .. tostring(v))
end
end
assert(rows[1].Id == "1", "Id should be '1'")
assert(rows[1].Name == "Test", "Name should be 'Test'")
assert(rows[1].Value == "100", "Value should be '100'")
end)
test("fromCSV real world mercenary file format", function()
local csv = [[#mercenary_profiles
Id ModifyStartCost ModifyStep ModifyLevelLimit Health ResistSheet WoundSlots MeleeDamage MeleeAccuracy RangeAccuracy ReceiveAmputationChance ReceiveWoundChanceMult AttackWoundChanceMult Dodge Los StarvationLimit PainThresholdLimit PainThresholdRegen TalentPerkId ActorId SkinIndex HairType HairColorHex VoiceBank Immunity CreatureClass
john_hawkwood_boss 20 0.1 140 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 8 16 crit 1.60 critchance 0.05 0.5 0.5 0.03 0.5 1.2 0.3 8 2200 16 2 talent_the_man_who_sold_the_world human_male 0 hair1 #633D08 player Human
francis_reid_daly 20 0.1 130 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 7 14 crit 1.70 critchance 0.05 0.5 0.4 0.04 0.9 1 0.3 8 2000 10 1 talent_weapon_durability human_male 0 player Human
]]
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
print("DEBUG: Number of rows: " .. #rows)
assert(#rows == 2, "Should have 2 data rows")
if #rows > 0 then
print("DEBUG: First row Id = " .. tostring(rows[1].Id))
print("DEBUG: First row ModifyStartCost = " .. tostring(rows[1].ModifyStartCost))
print("DEBUG: First row all string keys:")
for k, v in pairs(rows[1]) do
if type(k) == "string" then print(" " .. tostring(k) .. " = " .. tostring(v)) end
end
end
assert(rows[1].Id == "john_hawkwood_boss", "First row Id should be 'john_hawkwood_boss'")
assert(rows[1].ModifyStartCost == "20", "First row ModifyStartCost should be '20'")
assert(rows[2].Id == "francis_reid_daly", "Second row Id should be 'francis_reid_daly'")
end) end)
print("\nAll tests completed!") print("\nAll tests completed!")

View File

@@ -53,6 +53,34 @@ end
--- @field hasheader boolean? If true, first non-comment row is treated as headers (default: false). --- @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). --- @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. --- Parses CSV text into rows and fields using a minimal RFC 4180 state machine.
--- ---
--- Requirements/assumptions: --- Requirements/assumptions:
@@ -66,16 +94,22 @@ end
--- - Leading/trailing spaces are preserved; no trimming is performed. --- - Leading/trailing spaces are preserved; no trimming is performed.
--- - Empty fields and empty rows are preserved. --- - Empty fields and empty rows are preserved.
--- - The final row is emitted even if the text does not end with a newline. --- - 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 csv string The CSV text to parse.
--- @param options ParserOptions? Options for the parser --- @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) function fromCSV(csv, options)
if options == nil then options = {} end if options == nil then options = {} end
local delimiter = options.delimiter or ","
local hasheader = options.hasheader or false -- Validate options
local hascomments = options.hascomments or false 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 allRows = {}
local fields = {} local fields = {}
@@ -186,12 +220,13 @@ function fromCSV(csv, options)
row[headerName] = dataRow[j] row[headerName] = dataRow[j]
end end
end end
row.Headers = headers
table.insert(rows, row) table.insert(rows, row)
end end
return rows return rows, nil
end end
return allRows return allRows, nil
end end
--- Converts a table of rows back to CSV text format (RFC 4180 compliant). --- 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. --- - Fields are quoted if they contain the delimiter, newlines, or double quotes.
--- - Double quotes inside quoted fields are doubled (""). --- - Double quotes inside quoted fields are doubled ("").
--- - Fields are joined with the specified delimiter; rows are joined with newlines. --- - 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 rows table Array of rows, where each row is an array of field values.
--- @param delimiter string? The field delimiter (default: ","). --- @param options ParserOptions? Options for the parser
--- @return string CSV-formatted text. --- @return string? #CSV-formatted text, error string?
function toCSV(rows, delimiter) --- @return string? #Error message if conversion fails.
if delimiter == nil then delimiter = "," end 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 = {} 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 for _, row in ipairs(rows) do
local fieldStrings = {} local fieldStrings = {}
@@ -237,7 +304,7 @@ function toCSV(rows, delimiter)
table.insert(rowStrings, table.concat(fieldStrings, delimiter)) table.insert(rowStrings, table.concat(fieldStrings, delimiter))
end end
return table.concat(rowStrings, "\n") return table.concat(rowStrings, "\n"), nil
end end
-- String to number conversion helper -- String to number conversion helper