You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
448 lines
16 KiB
448 lines
16 KiB
--- @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
|
|
|