Skip to content

Commit

Permalink
moved out/refactored a lot of the encoder
Browse files Browse the repository at this point in the history
  • Loading branch information
FourierTransformer committed Mar 30, 2019
1 parent 17a25e6 commit 6f1b192
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 101 deletions.
122 changes: 122 additions & 0 deletions encoder.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
-- CSV Encoder for ftcsv

-- lua/luajit load compat
local M = {}
if type(jit) == 'table' or _ENV then
M.load = _G.load
else
M.load = loadstring
end

local function delimitField(field)
field = tostring(field)
if field:find('"') then
return field:gsub('"', '""')
else
return field
end
end

local function escapeHeadersForLuaGenerator(headers)
local escapedHeaders = {}
for i = 1, #headers do
if headers[i]:find('"') then
escapedHeaders[i] = headers[i]:gsub('"', '\\"')
else
escapedHeaders[i] = headers[i]
end
end
return escapedHeaders
end

-- a function that compiles some lua code to quickly print out the csv
local function csvLineGenerator(inputTable, delimiter, headers)
local escapedHeaders = escapeHeadersForLuaGenerator(headers)

local 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']]

local arguments = {}
arguments.t = inputTable
-- we shouldn't redefine delimitField for every line in
-- the csv, so we'll just pass it in here and reference it
arguments.delimitField = delimitField

return M.load(outputFunc), arguments, 0

end

local function validateHeaders(headers, inputTable)
for i = 1, #headers do
if inputTable[1][headers[i]] == nil then
error("ftcsv: the field '" .. headers[i] .. "' doesn't exist in the inputTable")
end
end
end

local function initializeOutputWithEscapedHeaders(escapedHeaders, delimiter)
local output = {}
output[1] = '"' .. table.concat(escapedHeaders, '"' .. delimiter .. '"') .. '"\r\n'
return output
end

local function escapeHeadersForOutput(headers)
local escapedHeaders = {}
for i = 1, #headers do
escapedHeaders[i] = delimitField(headers[i])
end
return escapedHeaders
end

local function extractHeadersFromTable(inputTable)
local headers = {}
for key, _ in pairs(inputTable[1]) do
headers[#headers+1] = key
end

-- lets make the headers alphabetical
table.sort(headers)

return headers
end

local function getHeadersFromOptions(options)
local headers = nil
if options then
if options.fieldsToKeep ~= nil then
assert(type(options.fieldsToKeep) == "table", "ftcsv only takes in a list (as a table) for the optional parameter 'fieldsToKeep'. You passed in '" .. tostring(options.headers) .. "' of type '" .. type(options.headers) .. "'.")
headers = options.fieldsToKeep
end
end
return headers
end

-- works really quickly with luajit-2.1, because table.concat life
local function encode(inputTable, delimiter, options)
-- delimiter MUST be one character
assert(#delimiter == 1 and type(delimiter) == "string", "the delimiter must be of string type and exactly one character")

local headers = getHeadersFromOptions(options)
if headers == nil then
headers = extractHeadersFromTable(inputTable)
end
validateHeaders(headers, inputTable)

local escapedHeaders = escapeHeadersForOutput(headers)
local output = initializeOutputWithEscapedHeaders(escapedHeaders, delimiter)

for i, line in csvLineGenerator(inputTable, delimiter, headers) do
output[i+1] = line
end

-- combine and return final string
return table.concat(output)
end

return encode
105 changes: 4 additions & 101 deletions ftcsv.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
local ftcsv = {
_VERSION = 'ftcsv 1.1.5',
_VERSION = 'ftcsv 1.2.0',
_DESCRIPTION = 'CSV library for Lua',
_URL = 'https://github.com/FourierTransformer/ftcsv',
_LICENSE = [[
Expand All @@ -24,16 +24,12 @@ local ftcsv = {
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
]],
encode = require("encoder")
}

-- lua 5.1 load compat
-- luajit/lua compatability layer
local M = {}
if type(jit) == 'table' or _ENV then
M.load = _G.load
else
M.load = loadstring
end

-- perf
local sbyte = string.byte
Expand Down Expand Up @@ -541,98 +537,5 @@ function ftcsv.parseLine(inputFile, delimiter, bufferSize, options)
end
end

-- a function that delimits " to "", used by the writer
local function delimitField(field)
field = tostring(field)
if field:find('"') then
return field:gsub('"', '""')
else
return field
end
end

-- a function that compiles some lua code to quickly print out the csv
local function writer(inputTable, dilimeter, headers)
-- they get re-created here if they need to be escaped so lua understands it based on how
-- they came in
for i = 1, #headers do
if inputTable[1][headers[i]] == nil then
error("ftcsv: the field '" .. headers[i] .. "' doesn't exist in the inputTable")
end
if headers[i]:find('"') then
headers[i] = headers[i]:gsub('"', '\\"')
end
end

local outputFunc = [[
local state, i = ...
local d = state.delimitField
i = i + 1;
if i > state.tableSize then return nil end;
return i, '"' .. d(state.t[i]["]] .. table.concat(headers, [["]) .. '"]] .. dilimeter .. [["' .. d(state.t[i]["]]) .. [["]) .. '"\r\n']]

-- print(outputFunc)

local state = {}
state.t = inputTable
state.tableSize = #inputTable
state.delimitField = delimitField

return M.load(outputFunc), state, 0

end

-- takes the values from the headers in the first row of the input table
local function extractHeaders(inputTable)
local headers = {}
for key, _ in pairs(inputTable[1]) do
headers[#headers+1] = key
end

-- lets make the headers alphabetical
table.sort(headers)

return headers
end

-- turns a lua table into a csv
-- works really quickly with luajit-2.1, because table.concat life
function ftcsv.encode(inputTable, delimiter, options)
local output = {}

-- dilimeter MUST be one character
assert(#delimiter == 1 and type(delimiter) == "string", "the delimiter must be of string type and exactly one character")

-- grab the headers from the options if they are there
local headers = nil
if options then
if options.fieldsToKeep ~= nil then
assert(type(options.fieldsToKeep) == "table", "ftcsv only takes in a list (as a table) for the optional parameter 'fieldsToKeep'. You passed in '" .. tostring(options.headers) .. "' of type '" .. type(options.headers) .. "'.")
headers = options.fieldsToKeep
end
end
if headers == nil then
headers = extractHeaders(inputTable)
end

-- newHeaders are needed if there are quotes within the header
-- because they need to be escaped
local newHeaders = {}
for i = 1, #headers do
if headers[i]:find('"') then
newHeaders[i] = headers[i]:gsub('"', '""')
else
newHeaders[i] = headers[i]
end
end
output[1] = '"' .. table.concat(newHeaders, '"' .. delimiter .. '"') .. '"\r\n'

-- add each line by line.
for i, line in writer(inputTable, delimiter, headers) do
output[i+1] = line
end
return table.concat(output)
end

return ftcsv

0 comments on commit 6f1b192

Please sign in to comment.