--- @brief Make a read-only view of a table. --- --- Returns a proxy table: reads pass through to the original table, writes raise an error. --- The original table is not modified and stays writable - changes to it show through the proxy. --- The proxy's metatable is protected (getmetatable returns false). --- --- Caveat: the proxy itself is empty - pairs(), next() and the # operator see no contents, --- and the table.* helpers built on them (keys, values, contains, isEmpty, ...) treat it --- as empty. Intended for protecting API namespace tables, not for data tables. --- --- @param tbl table: Table to protect --- @param name string: Optional name of the table, used in error messages --- @return table: Read-only proxy of the table 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() -- level 2 blames the assignment in the caller's code error(message, 2) end, __metatable = false, }) end --- @brief: Filters a table based on a predicate, all values in the table are run through this predicate; the ones that apply stay. --- Keys are preserved - filtering a numbered table may leave gaps, use table.ifilter for those. --- @param tbl table: Table to filter --- @param predicate function: The predicate/filtering function, e.g. function(v) return v <= 5 end - this keeps only values up to 5 --- @return table: Product of the operation (new 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 --- @brief: Filters a numbered table based on a predicate, all values in the table are run through this predicate; the ones that apply stay. Produces a new numbered table without gaps. --- @param tbl table: Numbered table to filter --- @param predicate function: The predicate/filtering function, e.g. function(v) return v <= 5 end - this keeps only values up to 5 --- @return table: Product of the operation (new table) function table.ifilter(tbl, predicate) if type(tbl) ~= "table" then error("table.ifilter: table 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 --- @brief: Maps the values in the table through a mapper function, preserving keys --- @param tbl table: Table to map --- @param mapper function: a mapping function, e.g. function(v) return v + 1 end --- @return table: Product of the operation (new table) 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 --- @brief: Maps the values in the table through a mapper function, preserving keys. If the function returns nil, the value is dropped. May leave gaps in a numbered table! --- @param tbl table: Table to map --- @param mapper function: a mapping function, e.g. function(v) return v + 1 end --- @return table: Product of the operation (new 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 --- @brief: Maps the values in the numbered table through a mapper function. If the function returns nil, the value is dropped. Result is a numbered table without gaps. --- @param tbl table: Numbered table to map --- @param mapper function: a mapping function, e.g. function(v) return v + 1 end --- @return table: Product of the operation (new table) function table.ifilterMap(tbl, mapper) if type(tbl) ~= "table" then error("table.ifilterMap: table 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 --- @brief Merge tables into a new table. The sequence part (consecutive positive integer keys --- starting at 1) of each table is appended in argument order; all other keys (including --- non-sequence numeric keys) are copied over, values from later arguments overwriting earlier --- ones. Not recursive: nested tables are copied by reference. Errors if an argument is not a table. --- --- Metatable is not copied. --- @param ... table: Tables to merge --- @return table: Product of the operation (new table) 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 --- @brief Merge tables recursively into a new table. Sequence parts are appended in argument --- order (like table.merge); for other keys, when both sides hold tables they are merged --- recursively, otherwise the value from the later argument overwrites the earlier one. --- The result shares no tables with the inputs (everything adopted is deep-copied). --- Errors if an argument is not a table, or when nesting exceeds a depth limit. --- --- Metatables are not copied. --- @param ... table: Tables to merge --- @return table: Product of the operation (new table) function table.deepMerge(...) return mergeRecurse(1, ...) end --- @brief Make a deep copy of a table --- --- Raises an error if the value is not a table --- --- @param value table: Table to copy --- @return table: Clone of the 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 --- @brief: Reduces all values in a table to a single value using a reducer function & initial value. Iteration order is unspecified. --- @param tbl table: Table to reduce --- @param reducer function: The reducer function, e.g. function(acc, v) return acc + v end - this computes the sum of all values in the table --- @param initialValue any: Starting value of the accumulator --- @return any: Result of the reduction 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 --- @brief: Reduces a numbered table to a single value using a reducer function & initial value, iterating the sequence part in order. Other keys are ignored. --- @param tbl table: Numbered table to reduce --- @param reducer function: The reducer function, e.g. function(acc, v) return acc .. v end - this concatenates all values in order --- @param initialValue any: Starting value of the accumulator --- @return any: Result of the reduction 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 --- @brief: Get the lowest of all values in a table (any table, all values are visited). --- Returns nil if the table is empty. If any value is NaN, the result is NaN. --- @param tbl table: Table to search --- @return any: The lowest value, or nil if the table is empty 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 -- NaN propagates end if min == nil or val < min then min = val end end return min end --- @brief: Get the highest of all values in a table (any table, all values are visited). --- Returns nil if the table is empty. If any value is NaN, the result is NaN. --- @param tbl table: Table to search --- @return any: The highest value, or nil if the table is empty 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 -- NaN propagates end if max == nil or val > max then max = val end end return max end --- @brief: Get the mean of all values in a table (any table, all values are visited). --- Returns nil if the table is empty. If any value is NaN, the result is NaN. --- @param tbl table: Table to average --- @return number|nil: Mean of the values, or nil if the table is empty 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 if sum == nil then sum = val else sum = sum + val end end if count == 0 then return nil end return sum / count end --- @brief: Get the sum of all values in a table (any table, all values are visited). --- Returns 0 if the table is empty. If any value is NaN, the result is NaN. --- @param tbl table: Table to sum --- @return number: Sum of the values, 0 for an empty table 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 --- @brief: Get an array of all keys in the table. Order is unspecified. --- @param tbl table: Table to take the keys from --- @return table: New numbered table with all keys 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 --- @brief: Get an array of all values in the table. Order is unspecified. --- @param tbl table: Table to take the values from --- @return table: New numbered table with all values 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 --- @brief: Check if a table contains a specific value. --- @param tbl table: Table to check --- @param value any: Value to search for (compared with ==) --- @return boolean: True if the table contains the given element 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 --- @brief: Find a table element that matches a predicate. Search order is unspecified --- (sequence part is in practice visited first, in order). --- @param tbl table: Table to check --- @param predicate function: Checker function --- @return any, any: Matching value, and its index (value, key); If not found, returns nil, nil. --- Check the key for nil to distinguish a stored false value 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 --- @brief: Check if a table is empty (has no elements). --- @param tbl table: Table to check --- @return boolean: True if empty table 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 --- @brief: Reverse an array. Returns a new array with elements in reverse order. --- @param tbl table: Table to reverse --- @return table: New table with reversed 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