mirror of
https://github.com/1bardesign/batteries.git
synced 2024-11-22 06:04:35 +00:00
635 lines
16 KiB
Lua
635 lines
16 KiB
Lua
--[[
|
|
extra table routines
|
|
]]
|
|
|
|
local path = (...):gsub("tablex", "")
|
|
local assert = require(path .. "assert")
|
|
|
|
--for spairs
|
|
--(can be replaced with eg table.sort to use that instead)
|
|
local sort = require(path .. "sort")
|
|
local spairs_sort = sort.stable_sort
|
|
|
|
--apply prototype to module if it isn't the global table
|
|
--so it works "as if" it was the global table api
|
|
--upgraded with these routines
|
|
local tablex = setmetatable({}, {
|
|
__index = table,
|
|
})
|
|
|
|
--alias
|
|
tablex.join = tablex.concat
|
|
|
|
--return the front element of a table
|
|
function tablex.front(t)
|
|
return t[1]
|
|
end
|
|
|
|
--return the back element of a table
|
|
function tablex.back(t)
|
|
return t[#t]
|
|
end
|
|
|
|
--remove the back element of a table and return it
|
|
function tablex.pop(t)
|
|
return table.remove(t)
|
|
end
|
|
|
|
--insert to the back of a table, returning the table for possible chaining
|
|
function tablex.push(t, v)
|
|
table.insert(t, v)
|
|
return t
|
|
end
|
|
|
|
--remove the front element of a table and return it
|
|
function tablex.shift(t)
|
|
return table.remove(t, 1)
|
|
end
|
|
|
|
--insert to the front of a table, returning the table for possible chaining
|
|
function tablex.unshift(t, v)
|
|
table.insert(t, 1, v)
|
|
return t
|
|
end
|
|
|
|
--swap two indices of a table
|
|
--(easier to read and generally less typing than the common idiom)
|
|
function tablex.swap(t, i, j)
|
|
t[i], t[j] = t[j], t[i]
|
|
end
|
|
|
|
--swap the element at i to the back of the table, and remove it
|
|
--avoids linear cost of removal at the expense of messing with the order of the table
|
|
function tablex.swap_and_pop(t, i)
|
|
tablex.swap(t, i, #t)
|
|
return tablex.pop(t)
|
|
end
|
|
|
|
--rotate the elements of a table t by amount slots
|
|
-- amount 1: {1, 2, 3, 4} -> {2, 3, 4, 1}
|
|
-- amount -1: {1, 2, 3, 4} -> {4, 1, 2, 3}
|
|
function tablex.rotate(t, amount)
|
|
if #t > 1 then
|
|
while amount >= 1 do
|
|
tablex.push(t, tablex.shift(t))
|
|
amount = amount - 1
|
|
end
|
|
while amount <= -1 do
|
|
tablex.unshift(t, tablex.pop(t))
|
|
amount = amount + 1
|
|
end
|
|
end
|
|
return t
|
|
end
|
|
|
|
--default comparison from sort.lua
|
|
local default_less = sort.default_less
|
|
|
|
--check if a function is sorted based on a "less" or "comes before" ordering comparison
|
|
--if any item is "less" than the item before it, we are not sorted
|
|
--(use stable_sort to )
|
|
function tablex.is_sorted(t, less)
|
|
less = less or default_less
|
|
for i = 1, #t - 1 do
|
|
if less(t[i + 1], t[i]) then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
--insert to the first position before the first larger element in the table
|
|
-- ({1, 2, 2, 3}, 2) -> {1, 2, 2, 2 (inserted here), 3}
|
|
--if this is used on an already sorted table, the table will remain sorted and not need re-sorting
|
|
--(you can sort beforehand if you don't know)
|
|
--return the table for possible chaining
|
|
function tablex.insert_sorted(t, v, less)
|
|
less = less or default_less
|
|
local low = 1
|
|
local high = #t
|
|
while low <= high do
|
|
local mid = math.floor((low + high) / 2)
|
|
local mid_val = t[mid]
|
|
if less(v, mid_val) then
|
|
high = mid - 1
|
|
else
|
|
low = mid + 1
|
|
end
|
|
end
|
|
table.insert(t, low, v)
|
|
return t
|
|
end
|
|
|
|
--find the index in a sequential table that a resides at
|
|
--or nil if nothing was found
|
|
function tablex.index_of(t, a)
|
|
if a == nil then return nil end
|
|
for i, b in ipairs(t) do
|
|
if a == b then
|
|
return i
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--find the key in a keyed table that a resides at
|
|
--or nil if nothing was found
|
|
function tablex.key_of(t, a)
|
|
if a == nil then return nil end
|
|
for k, v in pairs(t) do
|
|
if a == v then
|
|
return k
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--remove the first instance of value from a table (linear search)
|
|
--returns true if the value was removed, else false
|
|
function tablex.remove_value(t, a)
|
|
local i = tablex.index_of(t, a)
|
|
if i then
|
|
table.remove(t, i)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
--add a value to a table if it doesn't already exist (linear search)
|
|
--returns true if the value was added, else false
|
|
function tablex.add_value(t, a)
|
|
local i = tablex.index_of(t, a)
|
|
if not i then
|
|
table.insert(t, a)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
--get the next element in a sequential table
|
|
-- wraps around such that the next element to the last in sequence is the first
|
|
-- exists because builtin next may not behave as expected for mixed array/hash tables
|
|
-- if the element passed is not present or is nil, will also get the first element
|
|
-- but this should not be used to iterate the whole table; just use ipairs for that
|
|
function tablex.next_element(t, v)
|
|
local i = tablex.index_of(t, v)
|
|
--not present? just get the front of the table
|
|
if not i then
|
|
return tablex.front(t)
|
|
end
|
|
--(not using mathx to avoid inter-dependency)
|
|
i = (i % #t) + 1
|
|
return t[i]
|
|
end
|
|
|
|
function tablex.previous_element(t, v)
|
|
local i = tablex.index_of(t, v)
|
|
--not present? just get the front of the table
|
|
if not i then
|
|
return tablex.front(t)
|
|
end
|
|
--(not using mathx to avoid inter-dependency)
|
|
i = i - 1
|
|
if i == 0 then
|
|
i = #t
|
|
end
|
|
return t[i]
|
|
end
|
|
|
|
--note: keyed versions of the above aren't required; you can't double
|
|
--up values under keys
|
|
|
|
--helper for optionally passed random; defaults to love.math.random if present, otherwise math.random
|
|
local _global_random = math.random
|
|
if love and love.math and love.math.random then
|
|
_global_random = love.math.random
|
|
end
|
|
local function _random(min, max, r)
|
|
return r and r:random(min, max)
|
|
or _global_random(min, max)
|
|
end
|
|
|
|
--pick a random value from a table (or nil if it's empty)
|
|
function tablex.random_index(t, r)
|
|
if #t == 0 then
|
|
return 0
|
|
end
|
|
return _random(1, #t, r)
|
|
end
|
|
|
|
--pick a random value from a table (or nil if it's empty)
|
|
function tablex.pick_random(t, r)
|
|
if #t == 0 then
|
|
return nil
|
|
end
|
|
return t[tablex.random_index(t, r)]
|
|
end
|
|
|
|
--take a random value from a table (or nil if it's empty)
|
|
function tablex.take_random(t, r)
|
|
if #t == 0 then
|
|
return nil
|
|
end
|
|
return table.remove(t, tablex.random_index(t, r))
|
|
end
|
|
|
|
--return a random value from table t based on weights w provided (or nil empty)
|
|
-- w should be the same length as t
|
|
-- todo:
|
|
-- provide normalisation outside of this function, require normalised weights
|
|
function tablex.pick_weighted_random(t, w, r)
|
|
if #t == 0 then
|
|
return nil
|
|
end
|
|
if #w ~= #t then
|
|
error("tablex.pick_weighted_random weight and value tables should be the same length")
|
|
end
|
|
local sum = 0
|
|
for _, weight in ipairs(w) do
|
|
sum = sum + weight
|
|
end
|
|
local rnd = _random(nil, nil, r) * sum
|
|
sum = 0
|
|
for i, weight in ipairs(w) do
|
|
sum = sum + weight
|
|
if rnd <= sum then
|
|
return t[i]
|
|
end
|
|
end
|
|
--shouldn't get here but safety if using a random that returns >= 1
|
|
return tablex.back(t)
|
|
end
|
|
|
|
--shuffle the order of a table
|
|
function tablex.shuffle(t, r)
|
|
for i = 1, #t do
|
|
local j = _random(i, #t, r)
|
|
t[i], t[j] = t[j], t[i]
|
|
end
|
|
return t
|
|
end
|
|
|
|
--reverse the order of a table
|
|
function tablex.reverse(t)
|
|
for i = 1, #t / 2 do
|
|
local j = #t - i + 1
|
|
t[i], t[j] = t[j], t[i]
|
|
end
|
|
return t
|
|
end
|
|
|
|
--trim a table to a certain maximum length
|
|
function tablex.trim(t, l)
|
|
while #t > l do
|
|
table.remove(t)
|
|
end
|
|
return t
|
|
end
|
|
|
|
--collect all keys of a table into a sequential table
|
|
--(useful if you need to iterate non-changing keys often and want an nyi tradeoff;
|
|
-- this call will be slow but then following iterations can use ipairs)
|
|
function tablex.keys(t)
|
|
local r = {}
|
|
for k, v in pairs(t) do
|
|
table.insert(r, k)
|
|
end
|
|
return r
|
|
end
|
|
|
|
--collect all values of a keyed table into a sequential table
|
|
--(shallow copy if it's already sequential)
|
|
function tablex.values(t)
|
|
local r = {}
|
|
for k, v in pairs(t) do
|
|
table.insert(r, v)
|
|
end
|
|
return r
|
|
end
|
|
|
|
--collect all values over a range into a new sequential table
|
|
--useful where a range may have been modified to contain nils
|
|
-- range can be a number, where it is used as a numeric limit (ie [1-range])
|
|
-- range can be a table, where the sequential values are used as keys
|
|
function tablex.compact(t, range)
|
|
local r = {}
|
|
if type(range) == "table" then
|
|
for _, k in ipairs(range) do
|
|
local v = t[k]
|
|
if v then
|
|
table.insert(r, v)
|
|
end
|
|
end
|
|
elseif type(range) == "number" then
|
|
for i = 1, range do
|
|
local v = t[i]
|
|
if v then
|
|
table.insert(r, v)
|
|
end
|
|
end
|
|
else
|
|
error("tablex.compact - range must be a number or table", 2)
|
|
end
|
|
return r
|
|
|
|
end
|
|
|
|
--append sequence t2 into t1, modifying t1
|
|
function tablex.append_inplace(t1, t2, ...)
|
|
for i, v in ipairs(t2) do
|
|
table.insert(t1, v)
|
|
end
|
|
if ... then
|
|
return tablex.append_inplace(t1, ...)
|
|
end
|
|
return t1
|
|
end
|
|
|
|
--return a new sequence with the elements of both t1 and t2
|
|
function tablex.append(t1, ...)
|
|
local r = {}
|
|
tablex.append_inplace(r, t1, ...)
|
|
return r
|
|
end
|
|
|
|
--return a copy of a sequence with all duplicates removed
|
|
-- causes a little "extra" gc churn of one table to track the duplicates internally
|
|
function tablex.dedupe(t)
|
|
local seen = {}
|
|
local r = {}
|
|
for i, v in ipairs(t) do
|
|
if not seen[v] then
|
|
seen[v] = true
|
|
table.insert(r, v)
|
|
end
|
|
end
|
|
return r
|
|
end
|
|
|
|
--(might already exist depending on environment)
|
|
if not tablex.clear then
|
|
local imported
|
|
--pull in from luajit if possible
|
|
imported, tablex.clear = pcall(require, "table.clear")
|
|
if not imported then
|
|
--remove all values from a table
|
|
--useful when multiple references are being held
|
|
--so you cannot just create a new table
|
|
function tablex.clear(t)
|
|
assert:type(t, "table", "tablex.clear - t", 1)
|
|
local k = next(t)
|
|
while k ~= nil do
|
|
t[k] = nil
|
|
k = next(t)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Copy a table
|
|
-- See shallow_overlay to shallow copy into an existing table to avoid garbage.
|
|
function tablex.shallow_copy(t)
|
|
assert:type(t, "table", "tablex.shallow_copy - t", 1)
|
|
local into = {}
|
|
for k, v in pairs(t) do
|
|
into[k] = v
|
|
end
|
|
return into
|
|
end
|
|
|
|
--alias
|
|
tablex.copy = tablex.shallow_copy
|
|
|
|
--implementation for deep copy
|
|
--traces stuff that has already been copied, to handle circular references
|
|
local function _deep_copy_impl(t, already_copied)
|
|
local clone = t
|
|
if type(t) == "table" then
|
|
if already_copied[t] then
|
|
--something we've already encountered before
|
|
clone = already_copied[t]
|
|
elseif type(t.copy) == "function" then
|
|
--something that provides its own copy function
|
|
clone = t:copy()
|
|
assert:type(clone, "table", "member copy() function didn't return a copy")
|
|
else
|
|
--a plain table to clone
|
|
clone = {}
|
|
already_copied[t] = clone
|
|
for k, v in pairs(t) do
|
|
clone[k] = _deep_copy_impl(v, already_copied)
|
|
end
|
|
setmetatable(clone, getmetatable(t))
|
|
end
|
|
end
|
|
return clone
|
|
end
|
|
|
|
-- Recursively copy values of a table.
|
|
-- Retains the same keys as original table -- they're not cloned.
|
|
function tablex.deep_copy(t)
|
|
assert:type(t, "table", "tablex.deep_copy - t", 1)
|
|
return _deep_copy_impl(t, {})
|
|
end
|
|
|
|
-- Overlay tables directly onto one another, merging them together.
|
|
-- Doesn't merge tables within.
|
|
-- Takes as many tables as required,
|
|
-- overlays them in passed order onto the first,
|
|
-- and returns the first table.
|
|
function tablex.shallow_overlay(dest, ...)
|
|
assert:type(dest, "table", "tablex.shallow_overlay - dest", 1)
|
|
for i = 1, select("#", ...) do
|
|
local t = select(i, ...)
|
|
assert:type(t, "table", "tablex.shallow_overlay - ...", 1)
|
|
for k, v in pairs(t) do
|
|
dest[k] = v
|
|
end
|
|
end
|
|
return dest
|
|
end
|
|
|
|
tablex.overlay = tablex.shallow_overlay
|
|
|
|
-- Overlay tables directly onto one another, merging them together into something like a union.
|
|
-- Also overlays nested tables, but doesn't clone them (so a nested table may be added to dest).
|
|
-- Takes as many tables as required,
|
|
-- overlays them in passed order onto the first,
|
|
-- and returns the first table.
|
|
function tablex.deep_overlay(dest, ...)
|
|
assert:type(dest, "table", "tablex.deep_overlay - dest", 1)
|
|
for i = 1, select("#", ...) do
|
|
local t = select(i, ...)
|
|
assert:type(t, "table", "tablex.deep_overlay - ...", 1)
|
|
for k, v in pairs(t) do
|
|
if type(v) == "table" and type(dest[k]) == "table" then
|
|
tablex.deep_overlay(dest[k], v)
|
|
else
|
|
dest[k] = v
|
|
end
|
|
end
|
|
end
|
|
return dest
|
|
end
|
|
|
|
--collapse the first level of a table into a new table of reduced dimensionality
|
|
--will collapse {{1, 2}, 3, {4, 5, 6}} into {1, 2, 3, 4, 5, 6}
|
|
--useful when collating multiple result sets, or when you got 2d data when you wanted 1d data.
|
|
--in the former case you may just want to append_inplace though :)
|
|
--note that non-tabular elements in the base level are preserved,
|
|
-- but _all_ tables are collapsed; this includes any table-based types (eg a batteries.vec2),
|
|
-- so they can't exist in the base level
|
|
-- (... or at least, their non-ipairs members won't survive the collapse)
|
|
function tablex.collapse(t)
|
|
assert:type(t, "table", "tablex.collapse - t", 1)
|
|
local r = {}
|
|
for _, v in ipairs(t) do
|
|
if type(v) == "table" then
|
|
for _, w in ipairs(v) do
|
|
table.insert(r, w)
|
|
end
|
|
else
|
|
table.insert(r, v)
|
|
end
|
|
end
|
|
return r
|
|
end
|
|
|
|
--extract values of a table into nested tables of a set length
|
|
-- extract({1, 2, 3, 4}, 2) -> {{1, 2}, {3, 4}}
|
|
-- useful for working with "inlined" data in a more structured way
|
|
-- can use collapse (or functional.stitch) to reverse the process once you're done if needed
|
|
-- todo: support an ordered list of keys passed and extract them to names
|
|
function tablex.extract(t, n)
|
|
assert:type(t, "table", "tablex.extract - t", 1)
|
|
assert:type(n, "number", "tablex.extract - n", 1)
|
|
local r = {}
|
|
for i = 1, #t, n do
|
|
r[i] = {}
|
|
for j = 1, n do
|
|
table.insert(r[i], t[i + j])
|
|
end
|
|
end
|
|
return r
|
|
end
|
|
|
|
--check if two tables have equal contents at the first level
|
|
--slow, as it needs two loops
|
|
function tablex.shallow_equal(a, b)
|
|
if a == b then return true end
|
|
for k, v in pairs(a) do
|
|
if b[k] ~= v then
|
|
return false
|
|
end
|
|
end
|
|
-- second loop to ensure a isn't missing any keys from b.
|
|
-- we don't compare the values - if any are missing we're not equal
|
|
for k, v in pairs(b) do
|
|
if a[k] == nil then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
--check if two tables have equal contents all the way down
|
|
--slow, as it needs two potentially recursive loops
|
|
function tablex.deep_equal(a, b)
|
|
if a == b then return true end
|
|
--not equal on type
|
|
if type(a) ~= type(b) then
|
|
return false
|
|
end
|
|
--bottomed out
|
|
if type(a) ~= "table" then
|
|
return a == b
|
|
end
|
|
for k, v in pairs(a) do
|
|
if not tablex.deep_equal(v, b[k]) then
|
|
return false
|
|
end
|
|
end
|
|
-- second loop to ensure a isn't missing any keys from b
|
|
-- we don't compare the values - if any are missing we're not equal
|
|
for k, v in pairs(b) do
|
|
if a[k] == nil then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
--alias
|
|
tablex.flatten = tablex.collapse
|
|
|
|
--faster unpacking for known-length tables up to 8
|
|
--gets around nyi in luajit
|
|
--note: you can use a larger unpack than you need as the rest
|
|
-- can be discarded, but it "feels dirty" :)
|
|
|
|
function tablex.unpack2(t)
|
|
return t[1], t[2]
|
|
end
|
|
|
|
function tablex.unpack3(t)
|
|
return t[1], t[2], t[3]
|
|
end
|
|
|
|
function tablex.unpack4(t)
|
|
return t[1], t[2], t[3], t[4]
|
|
end
|
|
|
|
function tablex.unpack5(t)
|
|
return t[1], t[2], t[3], t[4], t[5]
|
|
end
|
|
|
|
function tablex.unpack6(t)
|
|
return t[1], t[2], t[3], t[4], t[5], t[6]
|
|
end
|
|
|
|
function tablex.unpack7(t)
|
|
return t[1], t[2], t[3], t[4], t[5], t[6], t[7]
|
|
end
|
|
|
|
function tablex.unpack8(t)
|
|
return t[1], t[2], t[3], t[4], t[5], t[6], t[7], t[8]
|
|
end
|
|
|
|
--internal: reverse iterator function
|
|
local function _ripairs_iter(t, i)
|
|
i = i - 1
|
|
local v = t[i]
|
|
if v then
|
|
return i, v
|
|
end
|
|
end
|
|
|
|
--iterator that works like ipairs, but in reverse order, with indices from #t to 1
|
|
--similar to ipairs, it will only consider sequential until the first nil value in the table.
|
|
function tablex.ripairs(t)
|
|
return _ripairs_iter, t, #t + 1
|
|
end
|
|
|
|
--works like pairs, but returns sorted table
|
|
-- generates a fair bit of garbage but very nice for more stable output
|
|
-- less function gets keys the of the table as its argument; if you want to sort on the values they map to then
|
|
-- you'll likely need a closure
|
|
function tablex.spairs(t, less)
|
|
less = less or default_less
|
|
--gather the keys
|
|
local keys = tablex.keys(t)
|
|
|
|
spairs_sort(keys, less)
|
|
|
|
local i = 0
|
|
return function()
|
|
i = i + 1
|
|
if keys[i] then
|
|
return keys[i], t[keys[i]]
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
return tablex
|