-- Basic functionality that should be standard in Lua -- Table / List -- Whether a table contains no keys function table.empty(t) return next(t)~=nil end -- Apply a function to each key in a table function table.map(f, ...) local ts = {...} local u = {} for k,_ in pairs(ts[1]) do local args = {} for j=1,#ts do args[j] = ts[j][i] end u[i] = f(unpack(args)) end return u end function table.map_list(f, ...) local ts = {...} local u = {} for i=1,#ts[1] do local args = {} for j=1,#ts do args[j] = ts[j][i] end u[i] = f(unpack(args)) end return u end -- Swap keys/values function table.swap(t) local u = {} for k,v in pairs(t) do u[v] = k end return u end -- Reverse a list function table.reverse(l) local m = {} for i=1,#l do m[#l-i+1] = l[i] end return m end -- Whether a table is a list/array (has only monotonic integer keys) function table.islist(t) local n = 0 for i,_ in pairs(t) do if type(i)~='number' or i%1~=0 then return false end n = n+1 end return n==#t end -- Append contents of other tables to first table function table.append(t, ...) local a = {...} for _,u in ipairs(a) do for _,v in ipairs(u) do table.insert(t,v) end end return t end -- Create a new table containing all keys from any number of tables -- latter tables in the arg list override prior ones -- overlaps, NOT appends, integer keys function table.join(...) local ts = {...} local w = {} for _,t in ipairs(ts) do for k,v in pairs(t) do w[k] = v end end return w end -- Whether a table contains a certain value in any key function table.contains(t,s) for _,v in pairs(t) do if v==s then return true end end return false end function table.contains_list(t,s) for _,v in ipairs(t) do if v==s then return true end end return false end -- Copy a table to another table function table.copy(t) local u = {} for k,v in pairs(t) do u[k] = v end return u end function table.copy_list(l) local m = {} for i,v in ipairs(l) do m[i] = v end return m end -- Sort a table in a new copy function table.sortcopy(t, f) local u = table.copy_list(t) table.sort(u, f) return u end -- Remove a value from a table function table.removevalue(t, r) local rem = {} for k,v in pairs(t) do if v==r then table.insert(rem, k) end end for _,k in ipairs(rem) do t[k] = nil end end function table.removevalue_list(t, r) for i = #t, 1, -1 do if t[i]==r then table.remove(t, i) end end end -- Export tables into formatted executable strings local function tabs(tabLevel) return (' '):rep(tabLevel) end local valueToString local function tableToString(t, tabLevel, seen) if type(t)~='table' or (getmetatable(t) and getmetatable(t).__tostring) then return tostring(t) elseif table.islist(t) then if #t==0 then return '{}' else local strs = {} local containsTables = false for _,v in ipairs(t) do if type(v)=='table' then containsTables = true end table.insert(strs, valueToString(v, tabLevel+1, seen)..',') end if containsTables or #t>3 then return '{\n'..tabs(tabLevel+1) ..table.concat(strs, '\n'..tabs(tabLevel+1)) ..'\n'..tabs(tabLevel)..'}' else return '{ '..table.concat(strs, ' ')..' }' end end else local containsNonStringKeys = false for k,v in pairs(t) do if type(k)~='string' or k:find('[^a-zA-Z0-9_]') then containsNonStringKeys = true elseif type(k)=='table' then error('table.tostring: table contains a table as key, cannot serialize') end end local strs = {} if containsNonStringKeys then for k,v in pairs(t) do table.insert(strs, '\n'..tabs(tabLevel+1) ..'['..valueToString(k, tabLevel+1, seen)..'] = ' ..valueToString(v, tabLevel+1, seen)..',') end else for k,v in pairs(t) do table.insert(strs, '\n'..tabs(tabLevel+1) ..k..' = '..valueToString(v, tabLevel+1, seen)..',') end end return '{'..table.concat(strs)..'\n'..tabs(tabLevel)..'}' end end valueToString = function(v, tabLevel, seen) local t = type(v) if t=='table' then if seen[v] then return 'nil --[[ already seen: '..tostring(v)..' ]]' else seen[v] = true return tableToString(v, tabLevel, seen) end elseif t=='string' then return '\''..string.escape(v)..'\'' elseif t=='number' or t=='boolean' then return tostring(v) else --error('table.tostring: table contains a '..t..' value, cannot serialize') return 'nil --[[ cannot serialize '..t..': '..tostring(v)..' ]]' end end function table.tostring(t) return tableToString(t, 0, {}) end -- String -- Split string into table by separator -- or by chars if no separator given -- if regex is not true, sep is treated as a regex pattern function string.split(str, sep, noregex) if type(str)~='string' then error('string.split: argument #1: expected string, got '..type(str), 2) end if sep==nil or sep=='' then local t = {} local ns = #str for x = 1, ns do table.insert(t, str:sub(x, x)) end return t elseif type(sep)=='string' then local t = {} if #str>0 then local first = 1 while true do local last, newfirst = str:find(sep, first, noregex) if not last then break end table.insert(t, str:sub(first, last-1)) first = newfirst+1 end table.insert(t, str:sub(first, #str)) end return t else error( 'string.split: argument #2: expected string or nil, got '..type(sep), 2) end end -- Split string to a list of char bytes function string.bytes(s) local b = {} for i=1,#s do local c = s:sub(i,i) table.insert(b, c:byte()) end return b end -- Trim leading and trailing whitespace function string.trim(s, ws) ws = ws or ' \t\r\n' return s:gsub('^['..ws..']+', ''):gsub('['..ws..']+$', '')..'' end -- String slicing and searching using [] operator local str_meta = getmetatable('') local str_meta_index_old= str_meta.__index function str_meta.__index(s,k) if type(k)=='string' then return str_meta_index_old[k] elseif type(k)=='number' then if k<0 then k = #s+k+1 end return string.sub(s,k,k) elseif type(k)=='table' then local a = k[1]<0 and (#s+k[1]+1) or k[1] local b = k[2]<0 and (#s+k[2]+1) or k[2] return string.sub(s,a,b) end end -- String iterator function string.chars(s) local i = 0 return function() i = i+1 if i<=#s then return s:sub(i,i) else return nil end end end -- Escape sequences local defaultEscapes = { ['\\'] = '\\\\', ['\''] = '\\\'', ['\"'] = '\\\"', ['\t'] = '\\t', ['\r'] = '\\r', ['\n'] = '\\n', ['\0'] = '\\0', } function string.escape(s, escapes) escapes = escapes or defaultEscapes local t = {} for i=1,#s do local c = s:sub(i,i) table.insert(t, escapes[c] or c) end return table.concat(t) end local defaultEscapeChar = '\\' local defaultUnescapes = { ['\\'] = '\\', ['\''] = '\'', ['\"'] = '\"', ['t'] = '\t', ['r'] = '\r', ['n'] = '\n', ['0'] = '\0', } function string.unescape(s, escapeChar, unescapes) escapeChar = escapeChar or defaultEscapeChar unescapes = unescapes or defaultUnescapes local t = {} local inEscape = false for i=1,#s do local c = s:sub(i,i) if inEscape then table.insert(t, unescapes[c] or error('string.unescape: invalid escape sequence: \'' ..escapeChar..c..'\'')) elseif c==escapeChar then inEscape = true else table.insert(t, c) end end return table.concat(t) end -- IO io = io or {} -- Read entire file at once, return nil,err if access failed function io.readall(filename) local fi,err = io.open(filename, 'rb') if not fi then return nil,err end local s = fi:read("*a") fi:close() return s end -- Write data to file all at once, return true if success / false,err if failure function io.writeall(filename, data) local fi,err = io.open(filename, 'wb') if not fi then return false,err end fi:write(data) fi:close() return true,nil end -- Math -- Round function math.round(x) return math.floor(x+0.5) end -- Mod that accounts for floating point inaccuracy function math.mod(a,b) local m = a%b if m==0 or math.abs(m)<1e-15 or math.abs(m-b)<1e-15 then return 0 else return m end end -- Clamp value between min and max function math.clamp(v, n, x) return math.min(x, math.max(v, n)) end