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.
260 lines
10 KiB
260 lines
10 KiB
--- 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
|
|
|