--- Make a read-only proxy of a table. Reads pass through; writes raise an error. --- Caveat: pairs(), next() and # see no contents (proxy is empty). Intended for API namespaces. function table.readonly(tbl, name) if type(tbl) ~= "table" then error("table.readonly: argument is not a table (got " .. type(tbl) .. ")") end if name ~= nil and type(name) ~= "string" then error("table.readonly: name is not a string (got " .. type(name) .. ")") end local message = "Attempt to update a read-only table" if name ~= nil then message = message .. " " .. name end return setmetatable({}, { __index = tbl, __newindex = function() error(message, 2) end, __metatable = false, }) end --- Filter a table by predicate, preserving keys. May leave gaps in a sequence table. function table.filter(tbl, predicate) if type(tbl) ~= "table" then error("table.filter: table argument is not a table (got " .. type(tbl) .. ")") end if type(predicate) ~= "function" then error("table.filter: predicate is not a function (got " .. type(predicate) .. ")") end local result = {} for k, v in pairs(tbl) do if predicate(v) then result[k] = v end end return result end --- Filter a sequence table by predicate, producing a new gapless sequence. function table.ifilter(tbl, predicate) if type(tbl) ~= "table" then error("table.ifilter: argument is not a table (got " .. type(tbl) .. ")") end if type(predicate) ~= "function" then error("table.ifilter: predicate is not a function (got " .. type(predicate) .. ")") end local result = {} for i = 1, #tbl do local v = tbl[i] if predicate(v) then result[#result + 1] = v end end return result end --- Map values through a function, preserving keys. function table.map(tbl, mapper) if type(tbl) ~= "table" then error("table.map: table argument is not a table (got " .. type(tbl) .. ")") end if type(mapper) ~= "function" then error("table.map: mapper is not a function (got " .. type(mapper) .. ")") end local result = {} for k, v in pairs(tbl) do result[k] = mapper(v) end return result end --- Map values through a function, dropping nils. May leave gaps in a sequence table. function table.filterMap(tbl, mapper) if type(tbl) ~= "table" then error("table.filterMap: table argument is not a table (got " .. type(tbl) .. ")") end if type(mapper) ~= "function" then error("table.filterMap: mapper is not a function (got " .. type(mapper) .. ")") end local result = {} for k, v in pairs(tbl) do local res = mapper(v) if res ~= nil then result[k] = res end end return result end --- Map a sequence through a function, dropping nils, producing a gapless sequence. function table.ifilterMap(tbl, mapper) if type(tbl) ~= "table" then error("table.ifilterMap: argument is not a table (got " .. type(tbl) .. ")") end if type(mapper) ~= "function" then error("table.ifilterMap: mapper is not a function (got " .. type(mapper) .. ")") end local result = {} for i = 1, #tbl do local res = mapper(tbl[i]) if res ~= nil then result[#result + 1] = res end end return result end --- Merge tables into a new table. Sequence parts are appended in order; other keys are --- copied with later arguments overwriting earlier ones. Not recursive; not deep. function table.merge(...) local result = {} local n = 0 for argi = 1, select("#", ...) do local tbl = select(argi, ...) if type(tbl) ~= "table" then error("table.merge: argument #" .. argi .. " is not a table (got " .. type(tbl) .. ")") end local len = 0 for i, v in ipairs(tbl) do n = n + 1 result[n] = v len = i end for k, v in pairs(tbl) do if not (math.type(k) == "integer" and k >= 1 and k <= len) then result[k] = v end end end return result end local MERGE_DEPTH_LIMIT = 100 local function deepCopy(value, level) if type(value) ~= "table" then return value end if level > MERGE_DEPTH_LIMIT then error("table.deepMerge/deepCopy recursion too deep") end local result = {} for k, v in pairs(value) do result[k] = deepCopy(v, level + 1) end return result end local function mergeRecurse(level, ...) if level > MERGE_DEPTH_LIMIT then error("table.deepMerge recursion too deep") end local result = {} local n = 0 for argi = 1, select('#', ...) do local tbl = select(argi, ...) if type(tbl) ~= "table" then error("table.deepMerge: argument #" .. argi .. " is not a table (got " .. type(tbl) .. ")") end local len = 0 for i, v in ipairs(tbl) do n = n + 1 result[n] = deepCopy(v, level) len = i end for k, v in pairs(tbl) do if not (math.type(k) == "integer" and k >= 1 and k <= len) then if type(v) == "table" and type(result[k]) == "table" then result[k] = mergeRecurse(level + 1, result[k], v) else result[k] = deepCopy(v, level) end end end end return result end --- Recursively merge tables. Nested tables on both sides are merged; other values are overwritten. --- The result shares no tables with the inputs (everything is deep-copied). function table.deepMerge(...) return mergeRecurse(1, ...) end --- Deep-copy a table at all levels (excluding metatables). function table.deepCopy(value) if type(value) ~= "table" then error("table.deepCopy: argument is not a table (got " .. type(value) .. ")") end return deepCopy(value, 1) end --- Reduce all values in a table to one using a function. Iteration order is unspecified. function table.reduce(tbl, reducer, initialValue) if type(tbl) ~= "table" then error("table.reduce: table argument is not a table (got " .. type(tbl) .. ")") end if type(reducer) ~= "function" then error("table.reduce: reducer is not a function (got " .. type(reducer) .. ")") end local accumulator = initialValue for _, v in pairs(tbl) do accumulator = reducer(accumulator, v) end return accumulator end --- Reduce a sequence table to one value, iterating in order. function table.ireduce(tbl, reducer, initialValue) if type(tbl) ~= "table" then error("table.ireduce: table argument is not a table (got " .. type(tbl) .. ")") end if type(reducer) ~= "function" then error("table.ireduce: reducer is not a function (got " .. type(reducer) .. ")") end local accumulator = initialValue for _, v in ipairs(tbl) do accumulator = reducer(accumulator, v) end return accumulator end --- Return the minimum value in a table. Returns nil for an empty table. NaN propagates. function table.min(tbl) if type(tbl) ~= "table" then error("table.min: argument is not a table (got " .. type(tbl) .. ")") end local min for _, val in pairs(tbl) do if val ~= val then return val end if min == nil or val < min then min = val end end return min end --- Return the maximum value in a table. Returns nil for an empty table. NaN propagates. function table.max(tbl) if type(tbl) ~= "table" then error("table.max: argument is not a table (got " .. type(tbl) .. ")") end local max for _, val in pairs(tbl) do if val ~= val then return val end if max == nil or val > max then max = val end end return max end --- Return the arithmetic mean of all values. Returns nil for an empty table. NaN propagates. function table.mean(tbl) if type(tbl) ~= "table" then error("table.mean: argument is not a table (got " .. type(tbl) .. ")") end local sum local count = 0 for _, val in pairs(tbl) do count = count + 1 sum = sum and sum + val or val end return count > 0 and sum / count or nil end --- Return the sum of all values. Returns 0 for an empty table. NaN propagates. function table.sum(tbl) if type(tbl) ~= "table" then error("table.sum: argument is not a table (got " .. type(tbl) .. ")") end local sum = 0 for _, val in pairs(tbl) do sum = sum + val end return sum end --- Return an array of all keys. Order is unspecified. function table.keys(tbl) if type(tbl) ~= "table" then error("table.keys: argument is not a table (got " .. type(tbl) .. ")") end local result = {} for k in pairs(tbl) do result[#result + 1] = k end return result end --- Return an array of all values. Order is unspecified. function table.values(tbl) if type(tbl) ~= "table" then error("table.values: argument is not a table (got " .. type(tbl) .. ")") end local result = {} for _, v in pairs(tbl) do result[#result + 1] = v end return result end --- Return true if the table contains the given value (compared with ==). function table.contains(tbl, value) if type(tbl) ~= "table" then error("table.contains: argument is not a table (got " .. type(tbl) .. ")") end for _, v in pairs(tbl) do if v == value then return true end end return false end --- Find a value matching a predicate. Returns (value, key) or (nil, nil) if not found. --- Check the returned key for nil to distinguish a stored false from "not found". function table.find(tbl, predicate) if type(tbl) ~= "table" then error("table.find: table argument is not a table (got " .. type(tbl) .. ")") end if type(predicate) ~= "function" then error("table.find: predicate is not a function (got " .. type(predicate) .. ")") end for k, v in pairs(tbl) do if predicate(v) then return v, k end end return nil, nil end --- Return true if the table has no elements. function table.isEmpty(tbl) if type(tbl) ~= "table" then error("table.isEmpty: argument is not a table (got " .. type(tbl) .. ")") end return next(tbl) == nil end --- Return a new array with elements in reverse order. function table.reverse(tbl) if type(tbl) ~= "table" then error("table.reverse: argument is not a table (got " .. type(tbl) .. ")") end local result = {} for i = #tbl, 1, -1 do result[#result + 1] = tbl[i] end return result end