mirror of
https://github.com/FourierTransformer/ftcsv.git
synced 2024-11-19 19:54:23 +00:00
added my initial tests
This commit is contained in:
parent
697856f0c2
commit
a79f9bdbed
175
encoder.lua
Normal file
175
encoder.lua
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
local ftcsv = {
|
||||||
|
_VERSION = 'ftcsv 1.2.0',
|
||||||
|
_DESCRIPTION = 'CSV library for Lua',
|
||||||
|
_URL = 'https://github.com/FourierTransformer/ftcsv',
|
||||||
|
_LICENSE = [[
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016-2020 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
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
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.
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
|
||||||
|
-- luajit/lua compatability layer
|
||||||
|
global jit: table
|
||||||
|
global _ENV: table
|
||||||
|
global loadstring: function(string)
|
||||||
|
local luaCompatibility: {string: function(string)} = {}
|
||||||
|
if type(jit) == 'table' or _ENV then
|
||||||
|
-- luajit and lua 5.2+
|
||||||
|
luaCompatibility.load = _G.load
|
||||||
|
else
|
||||||
|
-- lua 5.1
|
||||||
|
luaCompatibility.load = loadstring
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The ENCODER code is below here
|
||||||
|
-- This could be broken out, but is kept here for portability
|
||||||
|
|
||||||
|
local type EncoderOptions = record
|
||||||
|
fieldsToKeep: {string}
|
||||||
|
end
|
||||||
|
local type GeneratorArgs = record
|
||||||
|
t: {CSVRow}
|
||||||
|
delimitField: function(string): string
|
||||||
|
end
|
||||||
|
local type CSVRow = {string: any}
|
||||||
|
|
||||||
|
|
||||||
|
local function delimitField(field: string): string
|
||||||
|
field = tostring(field)
|
||||||
|
if field:find('"') then
|
||||||
|
return field:gsub('"', '""')
|
||||||
|
else
|
||||||
|
return field
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function escapeHeadersForLuaGenerator(headers: {string}): {string}
|
||||||
|
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: {CSVRow}, delimiter: string, headers: {string}): (function(string): (number, string), GeneratorArgs, number)
|
||||||
|
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: GeneratorArgs = {}
|
||||||
|
arguments.t = inputTable
|
||||||
|
-- we want to use the same delimitField throughout,
|
||||||
|
-- so we're just going to pass it in
|
||||||
|
arguments.delimitField = delimitField
|
||||||
|
|
||||||
|
return luaCompatibility.load(outputFunc), arguments, 0
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
local function validateHeaders(headers: {string}, inputTable: {CSVRow})
|
||||||
|
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: {string}, delimiter: string): {string}
|
||||||
|
local output = {}
|
||||||
|
output[1] = '"' .. table.concat(escapedHeaders, '"' .. delimiter .. '"') .. '"\r\n'
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
local function escapeHeadersForOutput(headers: {string}): {string}
|
||||||
|
local escapedHeaders = {}
|
||||||
|
for i = 1, #headers do
|
||||||
|
escapedHeaders[i] = delimitField(headers[i])
|
||||||
|
end
|
||||||
|
return escapedHeaders
|
||||||
|
end
|
||||||
|
|
||||||
|
local function extractHeadersFromTable(inputTable: {CSVRow}): {string}
|
||||||
|
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: EncoderOptions): {string}
|
||||||
|
local headers: {string} = 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.fieldsToKeep) .. "' of type '" .. type(options.fieldsToKeep) .. "'.")
|
||||||
|
headers = options.fieldsToKeep
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return headers
|
||||||
|
end
|
||||||
|
|
||||||
|
local function initializeGenerator(inputTable: {CSVRow}, delimiter: string, options: EncoderOptions): ({string}, {string})
|
||||||
|
-- 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)
|
||||||
|
return output, headers
|
||||||
|
end
|
||||||
|
|
||||||
|
-- works really quickly with luajit-2.1, because table.concat life
|
||||||
|
function ftcsv.encode(inputTable: {CSVRow}, delimiter: string, options: EncoderOptions): string
|
||||||
|
local output, headers = initializeGenerator(inputTable, delimiter, options)
|
||||||
|
|
||||||
|
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 ftcsv
|
||||||
|
|
140
ftcsv.lua
140
ftcsv.lua
@ -32,7 +32,16 @@ local sbyte = string.byte
|
|||||||
local ssub = string.sub
|
local ssub = string.sub
|
||||||
|
|
||||||
-- luajit/lua compatability layer
|
-- luajit/lua compatability layer
|
||||||
local luaCompatibility = {}
|
global jit: table
|
||||||
|
global _ENV: table
|
||||||
|
global loadstring: function(string)
|
||||||
|
local record LuaCompatibility
|
||||||
|
load: function(string)
|
||||||
|
LuaJIT: boolean
|
||||||
|
findClosingQuote: function(i: number, inputLength: number, inputString: string, quote: number, doubleQuoteEscape: boolean)
|
||||||
|
end
|
||||||
|
|
||||||
|
local luaCompatibility: LuaCompatibility = {}
|
||||||
if type(jit) == 'table' or _ENV then
|
if type(jit) == 'table' or _ENV then
|
||||||
-- luajit and lua 5.2+
|
-- luajit and lua 5.2+
|
||||||
luaCompatibility.load = _G.load
|
luaCompatibility.load = _G.load
|
||||||
@ -41,14 +50,34 @@ else
|
|||||||
luaCompatibility.load = loadstring
|
luaCompatibility.load = loadstring
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local type CSVRow = {string: any}
|
||||||
|
|
||||||
|
local record ParserOptions
|
||||||
|
loadFromString: boolean
|
||||||
|
rename: {string: string}
|
||||||
|
fieldsToKeep: {string}
|
||||||
|
ignoreQuotes: boolean
|
||||||
|
headerFunc: function(string): string
|
||||||
|
headers: boolean
|
||||||
|
inputLength: number
|
||||||
|
delimiter: string
|
||||||
|
totalColumnCount: number
|
||||||
|
bufferSize: number
|
||||||
|
buffered: boolean
|
||||||
|
endOfFile: boolean
|
||||||
|
rowOffset: number
|
||||||
|
headerField: {string}
|
||||||
|
headersMetamethod: function(table, string, string)
|
||||||
|
end
|
||||||
|
|
||||||
-- luajit specific speedups
|
-- luajit specific speedups
|
||||||
-- luajit performs faster with iterating over string.byte,
|
-- luajit performs faster with iterating over string.byte,
|
||||||
-- whereas vanilla lua performs faster with string.find
|
-- whereas vanilla lua performs faster with string.find
|
||||||
if type(jit) == 'table' then
|
if type(jit) == 'table' then
|
||||||
luaCompatibility.LuaJIT = true
|
luaCompatibility.LuaJIT = true
|
||||||
-- finds the end of an escape sequence
|
-- finds the end of an escape sequence
|
||||||
function luaCompatibility.findClosingQuote(i, inputLength, inputString, quote, doubleQuoteEscape)
|
function luaCompatibility.findClosingQuote(i: number, inputLength: number, inputString: string, quote: number, doubleQuoteEscape: boolean)
|
||||||
local currentChar, nextChar = sbyte(inputString, i), nil
|
local currentChar, nextChar: (number, number) = sbyte(inputString, i), nil
|
||||||
while i <= inputLength do
|
while i <= inputLength do
|
||||||
nextChar = sbyte(inputString, i+1)
|
nextChar = sbyte(inputString, i+1)
|
||||||
|
|
||||||
@ -73,8 +102,8 @@ else
|
|||||||
luaCompatibility.LuaJIT = false
|
luaCompatibility.LuaJIT = false
|
||||||
|
|
||||||
-- vanilla lua closing quote finder
|
-- vanilla lua closing quote finder
|
||||||
function luaCompatibility.findClosingQuote(i, inputLength, inputString, quote, doubleQuoteEscape)
|
function luaCompatibility.findClosingQuote(i: number, inputLength: number, inputString: string, quote: number, doubleQuoteEscape: boolean)
|
||||||
local j, difference
|
local j, difference: number, number
|
||||||
i, j = inputString:find('"+', i)
|
i, j = inputString:find('"+', i)
|
||||||
if j == nil then
|
if j == nil then
|
||||||
return nil
|
return nil
|
||||||
@ -90,9 +119,9 @@ end
|
|||||||
|
|
||||||
|
|
||||||
-- determine the real headers as opposed to the header mapping
|
-- determine the real headers as opposed to the header mapping
|
||||||
local function determineRealHeaders(headerField, fieldsToKeep)
|
local function determineRealHeaders(headerField: {string}, fieldsToKeep: {string: boolean}): {string}
|
||||||
local realHeaders = {}
|
local realHeaders = {}
|
||||||
local headerSet = {}
|
local headerSet: {string: boolean} = {}
|
||||||
for i = 1, #headerField do
|
for i = 1, #headerField do
|
||||||
if not headerSet[headerField[i]] then
|
if not headerSet[headerField[i]] then
|
||||||
if fieldsToKeep ~= nil and fieldsToKeep[headerField[i]] then
|
if fieldsToKeep ~= nil and fieldsToKeep[headerField[i]] then
|
||||||
@ -108,7 +137,7 @@ local function determineRealHeaders(headerField, fieldsToKeep)
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
local function determineTotalColumnCount(headerField, fieldsToKeep)
|
local function determineTotalColumnCount(headerField: {string}, fieldsToKeep: {string: boolean}): number
|
||||||
local totalColumnCount = 0
|
local totalColumnCount = 0
|
||||||
local headerFieldSet = {}
|
local headerFieldSet = {}
|
||||||
for _, header in pairs(headerField) do
|
for _, header in pairs(headerField) do
|
||||||
@ -123,7 +152,7 @@ local function determineTotalColumnCount(headerField, fieldsToKeep)
|
|||||||
return totalColumnCount
|
return totalColumnCount
|
||||||
end
|
end
|
||||||
|
|
||||||
local function generateHeadersMetamethod(finalHeaders)
|
local function generateHeadersMetamethod(finalHeaders: {string}): function(table, string, string)
|
||||||
-- if a header field tries to escape, we will simply return nil
|
-- if a header field tries to escape, we will simply return nil
|
||||||
-- the parser will still parse, but wont get the performance benefit of
|
-- the parser will still parse, but wont get the performance benefit of
|
||||||
-- having headers predefined
|
-- having headers predefined
|
||||||
@ -139,20 +168,20 @@ local function generateHeadersMetamethod(finalHeaders)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- main function used to parse
|
-- main function used to parse
|
||||||
local function parseString(inputString, i, options)
|
local function parseString(inputString: string, i: number, options: ParserOptions): ({string: string}, number, number)
|
||||||
|
|
||||||
-- keep track of my chars!
|
-- keep track of my chars!
|
||||||
local inputLength = options.inputLength or #inputString
|
local inputLength = options.inputLength or #inputString
|
||||||
local currentChar, nextChar = sbyte(inputString, i), nil
|
local currentChar, nextChar: number, number = sbyte(inputString, i), nil
|
||||||
local skipChar = 0
|
local skipChar = 0
|
||||||
local field
|
local field: string
|
||||||
local fieldStart = i
|
local fieldStart = i
|
||||||
local fieldNum = 1
|
local fieldNum = 1
|
||||||
local lineNum = 1
|
local lineNum = 1
|
||||||
local lineStart = i
|
local lineStart = i
|
||||||
local doubleQuoteEscape, emptyIdentified = false, false
|
local doubleQuoteEscape, emptyIdentified = false, false
|
||||||
|
|
||||||
local skipIndex
|
local skipIndex: number
|
||||||
local charPatternToSkip = "[" .. options.delimiter .. "\r\n]"
|
local charPatternToSkip = "[" .. options.delimiter .. "\r\n]"
|
||||||
|
|
||||||
--bytes
|
--bytes
|
||||||
@ -175,7 +204,7 @@ local function parseString(inputString, i, options)
|
|||||||
if headerField == nil then
|
if headerField == nil then
|
||||||
headerField = {}
|
headerField = {}
|
||||||
-- setup a metatable to simply return the key that's passed in
|
-- setup a metatable to simply return the key that's passed in
|
||||||
local headerMeta = {__index = function(_, key) return key end}
|
local headerMeta = {__index = function(_: table, key: string) return key end}
|
||||||
setmetatable(headerField, headerMeta)
|
setmetatable(headerField, headerMeta)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -208,7 +237,7 @@ local function parseString(inputString, i, options)
|
|||||||
if headerField[fieldNum] ~= nil then
|
if headerField[fieldNum] ~= nil then
|
||||||
outResults[lineNum][headerField[fieldNum]] = field
|
outResults[lineNum][headerField[fieldNum]] = field
|
||||||
else
|
else
|
||||||
error('ftcsv: too many columns in row ' .. options.rowOffset + lineNum)
|
error('ftcsv: too many columns in row ' .. (options.rowOffset + lineNum))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -265,7 +294,7 @@ local function parseString(inputString, i, options)
|
|||||||
fieldStart = i + 1 + skipChar
|
fieldStart = i + 1 + skipChar
|
||||||
lineStart = fieldStart
|
lineStart = fieldStart
|
||||||
else
|
else
|
||||||
error('ftcsv: too few columns in row ' .. options.rowOffset + lineNum)
|
error('ftcsv: too few columns in row ' .. (options.rowOffset + lineNum))
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
lineNum = lineNum + 1
|
lineNum = lineNum + 1
|
||||||
@ -290,7 +319,7 @@ local function parseString(inputString, i, options)
|
|||||||
outResults[lineNum] = nil
|
outResults[lineNum] = nil
|
||||||
return outResults, lineStart
|
return outResults, lineStart
|
||||||
else
|
else
|
||||||
error("ftcsv: can't find closing quote in row " .. options.rowOffset + lineNum ..
|
error("ftcsv: can't find closing quote in row " .. (options.rowOffset + lineNum) ..
|
||||||
". Try running with the option ignoreQuotes=true if the source incorrectly uses quotes.")
|
". Try running with the option ignoreQuotes=true if the source incorrectly uses quotes.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -321,14 +350,14 @@ local function parseString(inputString, i, options)
|
|||||||
if fieldNum == 1 and field == "" then
|
if fieldNum == 1 and field == "" then
|
||||||
outResults[lineNum] = nil
|
outResults[lineNum] = nil
|
||||||
else
|
else
|
||||||
error('ftcsv: too few columns in row ' .. options.rowOffset + lineNum)
|
error('ftcsv: too few columns in row ' .. (options.rowOffset + lineNum))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return outResults, i, totalColumnCount
|
return outResults, i, totalColumnCount
|
||||||
end
|
end
|
||||||
|
|
||||||
local function handleHeaders(headerField, options)
|
local function handleHeaders(headerField: {string}, options: ParserOptions): {string | number}
|
||||||
-- make sure a header isn't empty
|
-- make sure a header isn't empty
|
||||||
for _, headerName in ipairs(headerField) do
|
for _, headerName in ipairs(headerField) do
|
||||||
if #headerName == 0 then
|
if #headerName == 0 then
|
||||||
@ -370,19 +399,24 @@ local function handleHeaders(headerField, options)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- load an entire file into memory
|
-- load an entire file into memory
|
||||||
local function loadFile(textFile, amount)
|
local function loadFile(textFile: string, amount: string | number)
|
||||||
local file = io.open(textFile, "r")
|
local file = io.open(textFile, "r")
|
||||||
if not file then error("ftcsv: File not found at " .. textFile) end
|
if not file then error("ftcsv: File not found at " .. textFile) end
|
||||||
local lines = file:read(amount)
|
local lines: string
|
||||||
|
if amount is string then
|
||||||
|
file:read(amount)
|
||||||
|
else
|
||||||
|
file:read(amount)
|
||||||
|
end
|
||||||
if amount == "*all" then
|
if amount == "*all" then
|
||||||
file:close()
|
file:close()
|
||||||
end
|
end
|
||||||
return lines, file
|
return lines, file
|
||||||
end
|
end
|
||||||
|
|
||||||
local function initializeInputFromStringOrFile(inputFile, options, amount)
|
local function initializeInputFromStringOrFile(inputFile: string, options: ParserOptions, amount: string | number): (string, FILE)
|
||||||
-- handle input via string or file!
|
-- handle input via string or file!
|
||||||
local inputString, file
|
local inputString, file: (string, FILE)
|
||||||
if options.loadFromString then
|
if options.loadFromString then
|
||||||
inputString = inputFile
|
inputString = inputFile
|
||||||
else
|
else
|
||||||
@ -396,11 +430,11 @@ local function initializeInputFromStringOrFile(inputFile, options, amount)
|
|||||||
return inputString, file
|
return inputString, file
|
||||||
end
|
end
|
||||||
|
|
||||||
local function parseOptions(delimiter, options, fromParseLine)
|
local function parseOptions(delimiter: string, options: ParserOptions, fromParseLine: boolean): (ParserOptions, {string: boolean})
|
||||||
-- delimiter MUST be one character
|
-- delimiter MUST be one character
|
||||||
assert(#delimiter == 1 and type(delimiter) == "string", "the delimiter must be of string type and exactly one character")
|
assert(#delimiter == 1 and type(delimiter) == "string", "the delimiter must be of string type and exactly one character")
|
||||||
|
|
||||||
local fieldsToKeep = nil
|
local fieldsToKeep: {string: boolean} = nil
|
||||||
|
|
||||||
if options then
|
if options then
|
||||||
if options.headers ~= nil then
|
if options.headers ~= nil then
|
||||||
@ -452,7 +486,7 @@ local function parseOptions(delimiter, options, fromParseLine)
|
|||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function findEndOfHeaders(str, entireFile)
|
local function findEndOfHeaders(str: string, entireFile: boolean): number
|
||||||
local i = 1
|
local i = 1
|
||||||
local quote = sbyte('"')
|
local quote = sbyte('"')
|
||||||
local newlines = {
|
local newlines = {
|
||||||
@ -482,7 +516,8 @@ local function findEndOfHeaders(str, entireFile)
|
|||||||
return i
|
return i
|
||||||
end
|
end
|
||||||
|
|
||||||
local function determineBOMOffset(inputString)
|
|
||||||
|
local function determineBOMOffset(inputString: string): number
|
||||||
-- BOM files start with bytes 239, 187, 191
|
-- BOM files start with bytes 239, 187, 191
|
||||||
if sbyte(inputString, 1) == 239
|
if sbyte(inputString, 1) == 239
|
||||||
and sbyte(inputString, 2) == 187
|
and sbyte(inputString, 2) == 187
|
||||||
@ -493,12 +528,12 @@ local function determineBOMOffset(inputString)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function parseHeadersAndSetupArgs(inputString, delimiter, options, fieldsToKeep, entireFile)
|
local function parseHeadersAndSetupArgs(inputString: string, delimiter: string, options: ParserOptions, fieldsToKeep: {string: boolean}, entireFile: boolean): (number, ParserOptions, {string})
|
||||||
local startLine = determineBOMOffset(inputString)
|
local startLine = determineBOMOffset(inputString)
|
||||||
|
|
||||||
local endOfHeaderRow = findEndOfHeaders(inputString, entireFile)
|
local endOfHeaderRow = findEndOfHeaders(inputString, entireFile)
|
||||||
|
|
||||||
local parserArgs = {
|
local parserArgs: ParserOptions = {
|
||||||
delimiter = delimiter,
|
delimiter = delimiter,
|
||||||
headerField = nil,
|
headerField = nil,
|
||||||
fieldsToKeep = nil,
|
fieldsToKeep = nil,
|
||||||
@ -528,7 +563,7 @@ local function parseHeadersAndSetupArgs(inputString, delimiter, options, fieldsT
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- runs the show!
|
-- runs the show!
|
||||||
function ftcsv.parse(inputFile, delimiter, options)
|
function ftcsv.parse(inputFile: string, delimiter: string, options: ParserOptions)
|
||||||
local options, fieldsToKeep = parseOptions(delimiter, options, false)
|
local options, fieldsToKeep = parseOptions(delimiter, options, false)
|
||||||
|
|
||||||
local inputString = initializeInputFromStringOrFile(inputFile, options, "*all")
|
local inputString = initializeInputFromStringOrFile(inputFile, options, "*all")
|
||||||
@ -540,14 +575,14 @@ function ftcsv.parse(inputFile, delimiter, options)
|
|||||||
return output, finalHeaders
|
return output, finalHeaders
|
||||||
end
|
end
|
||||||
|
|
||||||
local function getFileSize (file)
|
local function getFileSize (file: FILE): number
|
||||||
local current = file:seek()
|
local current = file:seek()
|
||||||
local size = file:seek("end")
|
local size = file:seek("end")
|
||||||
file:seek("set", current)
|
file:seek("set", current)
|
||||||
return size
|
return size
|
||||||
end
|
end
|
||||||
|
|
||||||
local function determineAtEndOfFile(file, fileSize)
|
local function determineAtEndOfFile(file: FILE, fileSize: number): boolean
|
||||||
if file:seek() >= fileSize then
|
if file:seek() >= fileSize then
|
||||||
return true
|
return true
|
||||||
else
|
else
|
||||||
@ -555,14 +590,14 @@ local function determineAtEndOfFile(file, fileSize)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function initializeInputFile(inputString, options)
|
local function initializeInputFile(inputString: string, options: ParserOptions): (string, FILE)
|
||||||
if options.loadFromString == true then
|
if options.loadFromString == true then
|
||||||
error("ftcsv: parseLine currently doesn't support loading from string")
|
error("ftcsv: parseLine currently doesn't support loading from string")
|
||||||
end
|
end
|
||||||
return initializeInputFromStringOrFile(inputString, options, options.bufferSize)
|
return initializeInputFromStringOrFile(inputString, options, options.bufferSize)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ftcsv.parseLine(inputFile, delimiter, userOptions)
|
function ftcsv.parseLine(inputFile: string, delimiter: string, userOptions: ParserOptions)
|
||||||
local options, fieldsToKeep = parseOptions(delimiter, userOptions, true)
|
local options, fieldsToKeep = parseOptions(delimiter, userOptions, true)
|
||||||
local inputString, file = initializeInputFile(inputFile, options)
|
local inputString, file = initializeInputFile(inputFile, options)
|
||||||
|
|
||||||
@ -580,7 +615,7 @@ function ftcsv.parseLine(inputFile, delimiter, userOptions)
|
|||||||
|
|
||||||
inputString = ssub(inputString, endOfParsedInput)
|
inputString = ssub(inputString, endOfParsedInput)
|
||||||
local bufferIndex, returnedRowsCount = 0, 0
|
local bufferIndex, returnedRowsCount = 0, 0
|
||||||
local currentRow, buffer
|
local currentRow, buffer: string, string
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
-- check parsed buffer for value
|
-- check parsed buffer for value
|
||||||
@ -625,8 +660,16 @@ end
|
|||||||
-- The ENCODER code is below here
|
-- The ENCODER code is below here
|
||||||
-- This could be broken out, but is kept here for portability
|
-- This could be broken out, but is kept here for portability
|
||||||
|
|
||||||
|
local type EncoderOptions = record
|
||||||
|
fieldsToKeep: {string}
|
||||||
|
end
|
||||||
|
local type GeneratorArgs = record
|
||||||
|
t: {CSVRow}
|
||||||
|
delimitField: function(string): string
|
||||||
|
end
|
||||||
|
|
||||||
local function delimitField(field)
|
|
||||||
|
local function delimitField(field: string): string
|
||||||
field = tostring(field)
|
field = tostring(field)
|
||||||
if field:find('"') then
|
if field:find('"') then
|
||||||
return field:gsub('"', '""')
|
return field:gsub('"', '""')
|
||||||
@ -635,7 +678,7 @@ local function delimitField(field)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function escapeHeadersForLuaGenerator(headers)
|
local function escapeHeadersForLuaGenerator(headers: {string}): {string}
|
||||||
local escapedHeaders = {}
|
local escapedHeaders = {}
|
||||||
for i = 1, #headers do
|
for i = 1, #headers do
|
||||||
if headers[i]:find('"') then
|
if headers[i]:find('"') then
|
||||||
@ -648,7 +691,7 @@ local function escapeHeadersForLuaGenerator(headers)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- a function that compiles some lua code to quickly print out the csv
|
-- a function that compiles some lua code to quickly print out the csv
|
||||||
local function csvLineGenerator(inputTable, delimiter, headers)
|
local function csvLineGenerator(inputTable: {CSVRow}, delimiter: string, headers: {string}): (function(string): (number, string), GeneratorArgs, number)
|
||||||
local escapedHeaders = escapeHeadersForLuaGenerator(headers)
|
local escapedHeaders = escapeHeadersForLuaGenerator(headers)
|
||||||
|
|
||||||
local outputFunc = [[
|
local outputFunc = [[
|
||||||
@ -660,7 +703,7 @@ local function csvLineGenerator(inputTable, delimiter, headers)
|
|||||||
delimiter .. [["' .. args.delimitField(args.t[i]["]]) ..
|
delimiter .. [["' .. args.delimitField(args.t[i]["]]) ..
|
||||||
[["]) .. '"\r\n']]
|
[["]) .. '"\r\n']]
|
||||||
|
|
||||||
local arguments = {}
|
local arguments: GeneratorArgs = {}
|
||||||
arguments.t = inputTable
|
arguments.t = inputTable
|
||||||
-- we want to use the same delimitField throughout,
|
-- we want to use the same delimitField throughout,
|
||||||
-- so we're just going to pass it in
|
-- so we're just going to pass it in
|
||||||
@ -670,7 +713,7 @@ local function csvLineGenerator(inputTable, delimiter, headers)
|
|||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function validateHeaders(headers, inputTable)
|
local function validateHeaders(headers: {string}, inputTable: {CSVRow})
|
||||||
for i = 1, #headers do
|
for i = 1, #headers do
|
||||||
if inputTable[1][headers[i]] == nil then
|
if inputTable[1][headers[i]] == nil then
|
||||||
error("ftcsv: the field '" .. headers[i] .. "' doesn't exist in the inputTable")
|
error("ftcsv: the field '" .. headers[i] .. "' doesn't exist in the inputTable")
|
||||||
@ -678,13 +721,13 @@ local function validateHeaders(headers, inputTable)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function initializeOutputWithEscapedHeaders(escapedHeaders, delimiter)
|
local function initializeOutputWithEscapedHeaders(escapedHeaders: {string}, delimiter: string): {string}
|
||||||
local output = {}
|
local output = {}
|
||||||
output[1] = '"' .. table.concat(escapedHeaders, '"' .. delimiter .. '"') .. '"\r\n'
|
output[1] = '"' .. table.concat(escapedHeaders, '"' .. delimiter .. '"') .. '"\r\n'
|
||||||
return output
|
return output
|
||||||
end
|
end
|
||||||
|
|
||||||
local function escapeHeadersForOutput(headers)
|
local function escapeHeadersForOutput(headers: {string}): {string}
|
||||||
local escapedHeaders = {}
|
local escapedHeaders = {}
|
||||||
for i = 1, #headers do
|
for i = 1, #headers do
|
||||||
escapedHeaders[i] = delimitField(headers[i])
|
escapedHeaders[i] = delimitField(headers[i])
|
||||||
@ -692,7 +735,7 @@ local function escapeHeadersForOutput(headers)
|
|||||||
return escapedHeaders
|
return escapedHeaders
|
||||||
end
|
end
|
||||||
|
|
||||||
local function extractHeadersFromTable(inputTable)
|
local function extractHeadersFromTable(inputTable: {CSVRow}): {string}
|
||||||
local headers = {}
|
local headers = {}
|
||||||
for key, _ in pairs(inputTable[1]) do
|
for key, _ in pairs(inputTable[1]) do
|
||||||
headers[#headers+1] = key
|
headers[#headers+1] = key
|
||||||
@ -704,19 +747,20 @@ local function extractHeadersFromTable(inputTable)
|
|||||||
return headers
|
return headers
|
||||||
end
|
end
|
||||||
|
|
||||||
local function getHeadersFromOptions(options)
|
local function getHeadersFromOptions(options: EncoderOptions): {string}
|
||||||
local headers = nil
|
local headers: {string} = nil
|
||||||
if options then
|
if options then
|
||||||
if options.fieldsToKeep ~= nil then
|
if options.fieldsToKeep ~= nil then
|
||||||
assert(
|
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) .. "'.")
|
type(options.fieldsToKeep) == "table", "ftcsv only takes in a list (as a table) for the optional parameter 'fieldsToKeep'. You passed in '" .. tostring(options.fieldsToKeep) .. "' of type '" .. type(options.fieldsToKeep) .. "'.")
|
||||||
headers = options.fieldsToKeep
|
headers = options.fieldsToKeep
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return headers
|
return headers
|
||||||
end
|
end
|
||||||
|
|
||||||
local function initializeGenerator(inputTable, delimiter, options)
|
local function initializeGenerator(inputTable: {CSVRow}, delimiter: string, options: EncoderOptions): ({string}, {string})
|
||||||
-- delimiter MUST be one character
|
-- delimiter MUST be one character
|
||||||
assert(#delimiter == 1 and type(delimiter) == "string", "the delimiter must be of string type and exactly one character")
|
assert(#delimiter == 1 and type(delimiter) == "string", "the delimiter must be of string type and exactly one character")
|
||||||
|
|
||||||
@ -732,7 +776,7 @@ local function initializeGenerator(inputTable, delimiter, options)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- works really quickly with luajit-2.1, because table.concat life
|
-- works really quickly with luajit-2.1, because table.concat life
|
||||||
function ftcsv.encode(inputTable, delimiter, options)
|
function ftcsv.encode(inputTable: {CSVRow}, delimiter: string, options: EncoderOptions): string
|
||||||
local output, headers = initializeGenerator(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) do
|
||||||
|
Loading…
Reference in New Issue
Block a user