warframe


cerial. lua

Copyright 2018 wesley werner <wesley.werner@gmail.com> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. Serialize a Lua table into a CSV-like string. Handles standard types and circular table references. Does not serialize user data, functions or up-values.

Usage:

Performance vs pickle over 10000 encodes

Modified from original: https://gist.github.com/wesleywerner/3f1e3a64079d81511084925c465eb34d

Usage

Module

local csv = require('Module:CSV')

Documentation


Created with Docbunto

See Also

Code


---	cerial.lua
--	
--	Copyright 2018 wesley werner <wesley.werner@gmail.com>
--	This program is free software: you can redistribute it and/or modify
--	it under the terms of the GNU General Public License as published by
--	the Free Software Foundation, either version 3 of the License, or
--	any later version.
--	This program is distributed in the hope that it will be useful,
--	but WITHOUT ANY WARRANTY; without even the implied warranty of
--	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
--	GNU General Public License for more details.
--	You should have received a copy of the GNU General Public License
--	along with this program. If not, see http://www.gnu.org/licenses/.
--	Serialize a Lua table into a CSV-like string.
--	Handles standard types and circular table references.
--	Does not serialize user data, functions or up-values.
--	
--	Usage:
--	* local cerial = require("Module:CSV")
--	* local cerialstring = cerial.encode(table)
--	* local reconstructed = cerial.decode(cerialstring)
--	
--	Performance vs pickle over 10000 encodes
--	* cerial:
--	** serialized to 365 bytes
--	** real	0m0.854s
--	** user	0m0.852s
--	** sys	0m0.000s
--	* pickle:
--	** serialized to 463 bytes
--	** real	0m0.740s
--	** user	0m0.740s
--	** sys	0m0.004s
--	
--	Modified from original: https://gist.github.com/wesleywerner/3f1e3a64079d81511084925c465eb34d
--	
--	@module		csv
--	@alias		cerial
--	@author		[https://gist.github.com/wesleywerner wesleywerner]
--	@release	stable
--	<nowiki>

local cerial = { }
local tablerefs = { }
local sortedrefs = { }

function string.starts(String, Start)
	return string.sub(String,1,string.len(Start))==Start
end

function string.ends(String, End)
	return End=='' or string.sub(String,-string.len(End))==End
end

local function escapeCSV(s)
	if string.find(s, '[,"]') then
		s = '"' .. string.gsub(s, '"', '""') .. '"'
	end
	return s
end

local function fromCSV(s)
	s = s .. ','		-- ending comma
	local t = {}		-- table to collect fields
	local fieldstart = 1

	repeat
		-- next field is quoted? (start with `"'?)
		if string.find(s, '^"', fieldstart) then
			local a, c
			local i  = fieldstart
			repeat
				-- find closing quote
				a, i, c = string.find(s, '"("?)', i+1)
			until c ~= '"'	-- quote not followed by quote?
			if not i then error('unmatched "') end
			local f = string.sub(s, fieldstart+1, i-1)
			table.insert(t, (string.gsub(f, '""', '"')))
			fieldstart = string.find(s, ',', i) + 1
		else
			-- unquoted; find next comma
			local nexti = string.find(s, ',', fieldstart)
			table.insert(t, string.sub(s, fieldstart, nexti-1))
			fieldstart = nexti + 1
		end
	until fieldstart > string.len(s)

	-- remove blanks
	for n, l in ipairs(t) do
		if l:len() == 0 then
			table.remove(t, n)
		end
	end

  return t

end

function cerial.encode(target)

	tablerefs = { }
	sortedrefs = { }
	cerial.getRefs(target)
	local headers = cerial.headers()
	local details = cerial.details()
	return headers .. details

end

function cerial.decode(target)

	function splitlines(str)
		local t = {}
		local function helper(line)
			table.insert(t, line)
			return ""
		end
		helper((str:gsub("(.-)\r?\n", helper)))
		return t
	end

	local lines = splitlines(target)

	decodedrefs = { }
	local headers = { }

	for lineno, line in ipairs(lines) do

		-- split csv
		local fields = fromCSV(line)

		-- first field is record indicator
		local refno = tonumber(fields[1])

		-- build tables and headers
		for fieldno, fieldvalue in ipairs(fields) do

			-- build column names
			if refno == 0 then

				if fieldno == 1 then
					-- create a table reference
					table.insert(decodedrefs, { })
					table.insert(headers, { })
				else
					-- record column name
					table.insert(headers[#headers], fieldvalue)
				end

			end

		end

		-- process columns
		for fieldno, fieldvalue in ipairs(fields) do

			-- build column names
			if refno > 0 and fieldno > 1 then

				local columnname = headers[refno][fieldno - 1]

				-- assign field value
				local startref = string.starts(fieldvalue, "[")
				local endref = string.ends(fieldvalue, "]")

				-- use column index if a number, the name otherwise
				local parsedname = tonumber(columnname) or columnname

				if startref and endref then
					local subrefno = tonumber(fieldvalue:sub(2, -2))
					decodedrefs[refno][parsedname] = decodedrefs[subrefno]
					--print("link",parsedname,decodedrefs[subrefno], subrefno)
				else
					-- parse value as number
					local parsedvalue = tonumber(fieldvalue) or fieldvalue
					decodedrefs[refno][parsedname] = parsedvalue
				end

			end

		end

	end

	return decodedrefs[1]

end

function cerial.headers()

	local lines = { }

	-- headers
	for n, ref in ipairs(sortedrefs) do

		local items = { "00" }

		for k, v in pairs(ref) do
			table.insert(items, k)
		end

		table.insert(lines, table.concat(items, ","))

	end

	return table.concat(lines, "\n").."\n"

end

function cerial.details()

	local lines = { }

	for n, ref in ipairs(sortedrefs) do

		local refno = tablerefs[ref]
		local items = { string.format("%.2d", refno) }

		for k, v in pairs(ref) do
			local subref = tablerefs[v]
			if subref then
				table.insert(items, string.format("[%.2d]", subref))
			else
				table.insert(items, escapeCSV(tostring(v)))
			end
		end

		table.insert(lines, table.concat(items, ","))

	end

	return table.concat(lines, "\n").."\n"

end

function cerial.getRefs(target)

	local counter = 1
	local complete = false

	tablerefs[target] = counter
	counter = counter + 1

	-- root references
	for k, v in pairs(target) do
		if type(v) == "table" then
			if not tablerefs[v] then
				tablerefs[v] = counter
				counter = counter + 1
			end
		end
	end

	-- subtable references
	while not complete do
		complete = true
		for rv, rk in pairs(tablerefs) do
			for k, v in pairs(rv) do
				if type(v) == "table" then
					if not tablerefs[v] then
						complete = false
						tablerefs[v] = counter
						counter = counter + 1
					end
				end
			end
		end
	end

	-- sorted ref list
	for v, k in pairs(tablerefs) do
		sortedrefs[k] = v
	end

end

return cerial