Lua runner with rich builtin stdlib
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.
 
 
a/lua/stdlib/table.lua

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