mirror of
https://github.com/FourierTransformer/ftcsv.git
synced 2024-11-19 19:54:23 +00:00
Fix Header-less file bug and add ability to encode without Quotes (#34)
This commit is contained in:
parent
c858f99825
commit
5be2f78119
@ -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
|
||||
|
65
ftcsv.lua
65
ftcsv.lua
@ -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
|
||||
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 = {}
|
||||
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
|
||||
|
||||
|
@ -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] = {}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user