Fix Header-less file bug and add ability to encode without Quotes (#34)

This commit is contained in:
FourierTransformer 2023-02-02 23:19:00 -06:00 committed by GitHub
parent c858f99825
commit 5be2f78119
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 214 additions and 17 deletions

View File

@ -143,6 +143,13 @@ file:close()
local output = ftcsv.encode(everyUser, ",", {fieldsToKeep={"Name", "Phone", "City"}})
```
- `noQuotes`
if `noQuotes` is set to `true`, the output will not include quotes around fields.
```lua
local output = ftcsv.encode(everyUser, ",", {noQuotes=true})
```
## Error Handling

View File

@ -1,11 +1,11 @@
local ftcsv = {
_VERSION = 'ftcsv 1.2.0',
_VERSION = 'ftcsv 1.3.0',
_DESCRIPTION = 'CSV library for Lua',
_URL = 'https://github.com/FourierTransformer/ftcsv',
_LICENSE = [[
The MIT License (MIT)
Copyright (c) 2016-2020 Shakil Thakur
Copyright (c) 2016-2023 Shakil Thakur
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -329,18 +329,18 @@ local function parseString(inputString, i, options)
end
local function handleHeaders(headerField, options)
-- make sure a header isn't empty
for _, headerName in ipairs(headerField) do
if #headerName == 0 then
error('ftcsv: Cannot parse a file which contains empty headers')
end
end
-- for files where there aren't headers!
if options.headers == false then
for j = 1, #headerField do
headerField[j] = j
end
else
-- make sure a header isn't empty if there are headers
for _, headerName in ipairs(headerField) do
if #headerName == 0 then
error('ftcsv: Cannot parse a file which contains empty headers')
end
end
end
-- rename fields as needed!
@ -645,6 +645,17 @@ local function delimitField(field)
end
end
local function delimitAndQuoteField(field)
field = tostring(field)
if field:find('"') then
return '"' .. field:gsub('"', '""') .. '"'
elseif field:find('[\n,]') then
return '"' .. field .. '"'
else
return field
end
end
local function escapeHeadersForLuaGenerator(headers)
local escapedHeaders = {}
for i = 1, #headers do
@ -658,7 +669,7 @@ local function escapeHeadersForLuaGenerator(headers)
end
-- a function that compiles some lua code to quickly print out the csv
local function csvLineGenerator(inputTable, delimiter, headers)
local function csvLineGenerator(inputTable, delimiter, headers, options)
local escapedHeaders = escapeHeadersForLuaGenerator(headers)
local outputFunc = [[
@ -670,11 +681,26 @@ local function csvLineGenerator(inputTable, delimiter, headers)
delimiter .. [["' .. args.delimitField(args.t[i]["]]) ..
[["]) .. '"\r\n']]
if options and options.noQuotes == true then
outputFunc = [[
local args, i = ...
i = i + 1;
if i > ]] .. #inputTable .. [[ then return nil end;
return i, args.delimitField(args.t[i]["]] ..
table.concat(escapedHeaders, [["]) .. ']] ..
delimiter .. [[' .. args.delimitField(args.t[i]["]]) ..
[["]) .. '\r\n']]
end
local arguments = {}
arguments.t = inputTable
-- we want to use the same delimitField throughout,
-- so we're just going to pass it in
arguments.delimitField = delimitField
if options and options.noQuotes == true then
arguments.delimitField = delimitAndQuoteField
else
arguments.delimitField = delimitField
end
return luaCompatibility.load(outputFunc), arguments, 0
@ -688,17 +714,26 @@ local function validateHeaders(headers, inputTable)
end
end
local function initializeOutputWithEscapedHeaders(escapedHeaders, delimiter)
local function initializeOutputWithEscapedHeaders(escapedHeaders, delimiter, options)
local output = {}
output[1] = '"' .. table.concat(escapedHeaders, '"' .. delimiter .. '"') .. '"\r\n'
if options and options.noQuotes == true then
output[1] = table.concat(escapedHeaders, delimiter) .. '\r\n'
else
output[1] = '"' .. table.concat(escapedHeaders, '"' .. delimiter .. '"') .. '"\r\n'
end
return output
end
local function escapeHeadersForOutput(headers)
local function escapeHeadersForOutput(headers, options)
local escapedHeaders = {}
local delimitField = delimitField
if options and options.noQuotes == true then
delimitField = delimitAndQuoteField
end
for i = 1, #headers do
escapedHeaders[i] = delimitField(headers[i])
end
return escapedHeaders
end
@ -736,8 +771,8 @@ local function initializeGenerator(inputTable, delimiter, options)
end
validateHeaders(headers, inputTable)
local escapedHeaders = escapeHeadersForOutput(headers)
local output = initializeOutputWithEscapedHeaders(escapedHeaders, delimiter)
local escapedHeaders = escapeHeadersForOutput(headers, options)
local output = initializeOutputWithEscapedHeaders(escapedHeaders, delimiter, options)
return output, headers
end
@ -745,7 +780,7 @@ end
function ftcsv.encode(inputTable, delimiter, options)
local output, headers = initializeGenerator(inputTable, delimiter, options)
for i, line in csvLineGenerator(inputTable, delimiter, headers) do
for i, line in csvLineGenerator(inputTable, delimiter, headers, options) do
output[i+1] = line
end

View File

@ -164,6 +164,21 @@ describe("csv features", function()
assert.are.same(expected, actual)
end)
it("should handle files without headers with an empty header field", function()
local expected = {}
expected[1] = {}
expected[1][1] = "apple"
expected[1][2] = "banana"
expected[1][3] = ""
expected[2] = {}
expected[2][1] = "diamond"
expected[2][2] = "emerald"
expected[2][3] = "pearl"
local options = {loadFromString=true, headers=false}
local actual = ftcsv.parse("apple>banana>\ndiamond>emerald>pearl", ">", options)
assert.are.same(expected, actual)
end)
it("should handle files without (headers and newlines)", function()
local expected = {}
expected[1] = {}
@ -175,6 +190,17 @@ describe("csv features", function()
assert.are.same(expected, actual)
end)
it("should handle files without (headers and newlines) with an empty header field", function()
local expected = {}
expected[1] = {}
expected[1][1] = "apple"
expected[1][2] = "banana"
expected[1][3] = ""
local options = {loadFromString=true, headers=false}
local actual = ftcsv.parse("apple>banana>", ">", options)
assert.are.same(expected, actual)
end)
it("should handle files with quotes and without (headers and newlines)", function()
local expected = {}
expected[1] = {}
@ -186,6 +212,17 @@ describe("csv features", function()
assert.are.same(expected, actual)
end)
it("should handle files with quotes and without (headers and newlines) with an empty header field", function()
local expected = {}
expected[1] = {}
expected[1][1] = "apple"
expected[1][2] = "banana"
expected[1][3] = ""
local options = {loadFromString=true, headers=false}
local actual = ftcsv.parse('"apple">"banana">', ">", options)
assert.are.same(expected, actual)
end)
it("should handle files with quotes and without (headers and newlines)", function()
local expected = {}
expected[1] = {}
@ -201,6 +238,21 @@ describe("csv features", function()
assert.are.same(expected, actual)
end)
it("should handle files with quotes and without (headers and newlines) with an empty header field", function()
local expected = {}
expected[1] = {}
expected[1][1] = "apple"
expected[1][2] = "banana"
expected[1][3] = ""
expected[2] = {}
expected[2][1] = "diamond"
expected[2][2] = "emerald"
expected[2][3] = "pearl"
local options = {loadFromString=true, headers=false}
local actual = ftcsv.parse('"apple">"banana">\n"diamond">"emerald">"pearl"', ">", options)
assert.are.same(expected, actual)
end)
it("should handle files without (headers and newlines) w/newline at end", function()
local expected = {}
expected[1] = {}
@ -212,6 +264,17 @@ describe("csv features", function()
assert.are.same(expected, actual)
end)
it("should handle files without (headers and newlines) w/newline at end with an empty header field", function()
local expected = {}
expected[1] = {}
expected[1][1] = "apple"
expected[1][2] = "banana"
expected[1][3] = ""
local options = {loadFromString=true, headers=false}
local actual = ftcsv.parse("apple>banana>\n", ">", options)
assert.are.same(expected, actual)
end)
it("should handle files without (headers and newlines) w/crlf", function()
local expected = {}
expected[1] = {}
@ -223,6 +286,17 @@ describe("csv features", function()
assert.are.same(expected, actual)
end)
it("should handle files without (headers and newlines) w/crlf with an empty header field", function()
local expected = {}
expected[1] = {}
expected[1][1] = "apple"
expected[1][2] = "banana"
expected[1][3] = ""
local options = {loadFromString=true, headers=false}
local actual = ftcsv.parse("apple>banana>\r\n", ">", options)
assert.are.same(expected, actual)
end)
it("should handle files without (headers and newlines) w/cr", function()
local expected = {}
expected[1] = {}
@ -234,6 +308,18 @@ describe("csv features", function()
assert.are.same(expected, actual)
end)
it("should handle files without (headers and newlines) w/cr with an empty header field", function()
local expected = {}
expected[1] = {}
expected[1][1] = "apple"
expected[1][2] = "banana"
expected[1][3] = ""
local options = {loadFromString=true, headers=false}
local actual = ftcsv.parse("apple>banana>\r", ">", options)
assert.are.same(expected, actual)
end)
it("should handle only renaming fields from files without headers", function()
local expected = {}
expected[1] = {}
@ -249,6 +335,21 @@ describe("csv features", function()
assert.are.same(expected, actual)
end)
it("should handle only renaming fields from files without headers with an empty header field", function()
local expected = {}
expected[1] = {}
expected[1].a = "apple"
expected[1].b = "banana"
expected[1].c = ""
expected[2] = {}
expected[2].a = "diamond"
expected[2].b = "emerald"
expected[2].c = "pearl"
local options = {loadFromString=true, headers=false, rename={"a","b","c"}}
local actual = ftcsv.parse("apple>banana>\ndiamond>emerald>pearl", ">", options)
assert.are.same(expected, actual)
end)
it("should handle only renaming fields from files without headers and only keeping a few fields", function()
local expected = {}
expected[1] = {}
@ -262,6 +363,19 @@ describe("csv features", function()
assert.are.same(expected, actual)
end)
it("should handle only renaming fields from files without headers and only keeping a few fields with an empty header field", function()
local expected = {}
expected[1] = {}
expected[1].a = "apple"
expected[1].b = ""
expected[2] = {}
expected[2].a = "diamond"
expected[2].b = "emerald"
local options = {loadFromString=true, headers=false, rename={"a","b","c"}, fieldsToKeep={"a","b"}}
local actual = ftcsv.parse("apple>>carrot\ndiamond>emerald>pearl", ">", options)
assert.are.same(expected, actual)
end)
it("should handle if the number of renames doesn't equal the number of fields", function()
local expected = {}
expected[1] = {}
@ -318,6 +432,30 @@ describe("csv features", function()
assert.are.same(expected, actual)
end)
it("should handle encoding files (str test)", function()
local expected = '"a","b","c","d"\r\n"1","","foo","""quoted"""\r\n'
output = ftcsv.encode({
{ a = 1, b = '', c = 'foo', d = '"quoted"' };
}, ',')
assert.are.same(expected, output)
end)
it("should handle encoding files without quotes (str test)", function()
local expected = 'a,b,c,d\r\n1,,foo,"""quoted"""\r\n'
output = ftcsv.encode({
{ a = 1, b = '', c = 'foo', d = '"quoted"' };
}, ',', {noQuotes=true})
assert.are.same(expected, output)
end)
it("should handle encoding files without quotes with certain fields to keep (str test)", function()
local expected = "b,c\r\n,foo\r\n"
output = ftcsv.encode({
{ a = 1, b = '', c = 'foo', d = '"quoted"' };
}, ',', {noQuotes=true, fieldsToKeep={"b", "c"}})
assert.are.same(expected, output)
end)
it("should handle headers attempting to escape", function()
local expected = {}
expected[1] = {}

View File

@ -87,3 +87,20 @@ describe("csv encode", function()
end)
end
end)
describe("csv encode without quotes", function()
for _, value in ipairs(files) do
it("should handle " .. value, function()
local jsonFile = loadFile("spec/json/" .. value .. ".json")
local jsonDecode = cjson.decode(jsonFile)
-- local parse = staecsv:ftcsv(contents, ",")
local reEncodedNoQuotes = ftcsv.parse(ftcsv.encode(jsonDecode, ",", {noQuotes=true}), ",", {loadFromString=true})
-- local f = csv.openstring(contents, {separator=",", header=true})
-- local parse = {}
-- for fields in f:lines() do
-- parse[#parse+1] = fields
-- end
assert.are.same(jsonDecode, reEncodedNoQuotes)
end)
end
end)