Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
--------------------------------------------------------------------------------
--- Small table utilities
-- @module lua-nucleo.table-utils
-- This file is a part of lua-nucleo library
-- @copyright lua-nucleo authors (see file `COPYRIGHT` for the license)
--------------------------------------------------------------------------------
local setmetatable, error, pairs, ipairs, tostring, select, type, assert,
unpack
= setmetatable, error, pairs, ipairs, tostring, select, type, assert,
unpack or table.unpack
local rawget = rawget
local table_insert, table_remove = table.insert, table.remove
local math_min, math_max = math.min, math.max
--------------------------------------------------------------------------------
local arguments,
optional_arguments,
method_arguments
= import 'lua-nucleo/args.lua'
{
'arguments',
'optional_arguments',
'method_arguments'
}
local is_number,
is_table
= import 'lua-nucleo/type.lua'
{
'is_number',
'is_table'
}
local assert_is_table
= import 'lua-nucleo/typeassert.lua'
{
'assert_is_table'
}
--------------------------------------------------------------------------------
--- Warning: it is possible to corrupt this with rawset and debug.setmetatable.
local empty_table = setmetatable(
{ },
{
__newindex = function(t, k, v)
error("attempted to change the empty table", 2)
end;
__metatable = "empty_table";
}
)
--- @param t
-- @param s
-- @param ...
local function toverride_many(t, s, ...)
if s then
for k, v in pairs(s) do
t[k] = v
end
-- Recursion is usually faster than calling select()
return toverride_many(t, ...)
end
return t
end
--- @param t
-- @param s
-- @param ...
local function tappend_many(t, s, ...)
if s then
for k, v in pairs(s) do
if t[k] == nil then
t[k] = v
else
error("attempted to override table key `" .. tostring(k) .. "'", 2)
end
end
-- Recursion is usually faster than calling select()
return tappend_many(t, ...)
end
return t
end
--- @param t
-- @param s
-- @param ...
local function tijoin_many(t, s, ...)
if s then
-- Note: can't use ipairs() since we want to support tijoin_many(t, t)
for i = 1, #s do
t[#t + 1] = s[i]
end
-- Recursion is usually faster than calling select()
return tijoin_many(t, ...)
end
return t
end
--- Keys are ordered in undetermined order
-- @param t
local tkeys = function(t)
local r = { }
for k, v in pairs(t) do
r[#r + 1] = k
end
return r
end
--- Values are ordered in undetermined order
-- @param t
local tvalues = function(t)
local r = { }
for k, v in pairs(t) do
r[#r + 1] = v
end
return r
end
--- Keys and values are ordered in undetermined order
-- @param t
local tkeysvalues = function(t)
local keys, values = { }, { }
for k, v in pairs(t) do
keys[#keys + 1] = k
values[#values + 1] = v
end
return keys, values
end
--- If table contains multiple keys with the same value,
-- only one key is stored in the result, picked in undetermined way.
-- @param t
local tflip = function(t)
local r = { }
for k, v in pairs(t) do
r[v] = k
end
return r
end
--- If table contains multiple keys with the same value,
-- only one key is stored in the result, picked in undetermined way.
-- @param t
local tflip_inplace = function(t)
for k, v in pairs(t) do
t[v] = k
end
return t
end
--- If table contains multiple keys with the same value,
-- only the last such key (highest one) is stored in the result.
-- @param t
local tiflip = function(t)
local r = { }
for i = 1, #t do
r[t[i]] = i
end
return r
end
--- @param t
local tset = function(t)
local r = { }
for k, v in pairs(t) do
r[v] = true
end
return r
end
--- @param t
local tiset = function(t)
local r = { }
for i = 1, #t do
r[t[i]] = true
end
return r
end
--- @param t
-- @param a
-- @param ...
local function tiinsert_args(t, a, ...)
if a ~= nil then
t[#t + 1] = a
-- Recursion is usually faster than calling select() in a loop.
return tiinsert_args(t, ...)
end
return t
end
--- @param fn
-- @param t
-- @param ...
local timap_inplace = function(fn, t, ...)
for i = 1, #t do
t[i] = fn(t[i], ...)
end
return t
end
--- @param fn
-- @param t
-- @param ...
local timap = function(fn, t, ...)
local r = { }
for i = 1, #t do
r[i] = fn(t[i], ...)
end
return r
end
--- @param fn
-- @param t
-- @param ...
local timap_sliding = function(fn, t, ...)
local r = {}
for i = 1, #t do
tiinsert_args(r, fn(t[i], ...))
end
return r
end
--- @param fn
-- @param t
-- @param ...
local tiwalk = function(fn, t, ...)
for i = 1, #t do
fn(t[i], ...)
end
end
--- @param fn
local tiwalker = function(fn)
return function(t)
for i = 1, #t do
fn(t[i])
end
end
end
--- @param fn
-- @param t
local twalk_pairs = function(fn, t)
for k, v in pairs(t) do
fn(k, v)
end
end
local tequals = function(lhs, rhs)
for k, v in pairs(lhs) do
if v ~= rhs[k] then
return false
end
end
for k, v in pairs(rhs) do
if lhs[k] == nil then
return false
end
end
return true
end
--- @param t
local tiunique = function(t)
return tkeys(tiflip(t))
end
--- Deprecated, use tgenerate_1d_linear instead
-- @param n
-- @param generator
-- @param ...
local tgenerate_n = function(n, generator, ...)
local r = { }
for i = 1, n do
r[i] = generator(...)
end
return r
end
--- @param n
-- @param fn
-- @param ...
local tgenerate_1d_linear = function(n, fn, ...)
local r = { }
for i = 1, n do
r[#r + 1] = fn(i, ...)
end
return r
end
--- @param w
--- @param h
-- @param fn
-- @param ...
local tgenerate_2d_linear = function(w, h, fn, ...)
local r = { }
for y = 1, h do
for x = 1, w do
r[#r + 1] = fn(x, y, ...)
end
end
return r
end
--- @param t
-- @param init
local taccumulate = function(t, init)
local sum = init or 0
for k, v in pairs(t) do
sum = sum + v
end
return sum
end
local tnormalize, tnormalize_inplace
do
local impl = function(t, r, sum)
sum = sum or taccumulate(t)
for k, v in pairs(t) do
r[k] = v / sum
end
return r
end
--- @param t
-- @param sum
-- @local
tnormalize = function(t, sum)
return impl(t, { }, sum)
end
--- @param t
-- @param sum
-- @local
tnormalize_inplace = function(t, sum)
return impl(t, t, sum)
end
end
local tclone
do
local function impl(t, visited)
local t_type = type(t)
if t_type ~= "table" then
return t
end
assert(not visited[t], "recursion detected")
visited[t] = true
local r = { }
for k, v in pairs(t) do
r[impl(k, visited)] = impl(v, visited)
end
visited[t] = nil
return r
end
--- @param t
-- @local
tclone = function(t)
return impl(t, { })
end
end
--- Slow
-- @param t
-- @local
local tcount_elements = function(t)
local n = 0
for _ in pairs(t) do
n = n + 1
end
return n
end
--- @param fn
-- @param t
local tremap_to_array = function(fn, t)
local r = { }
for k, v in pairs(t) do
r[#r + 1] = fn(k, v)
end
return r
end
--- @param fn
-- @param t
-- @param ...
local tmap_values = function(fn, t, ...)
local r = { }
for k, v in pairs(t) do
r[k] = fn(v, ...)
end
return r
end
--------------------------------------------------------------------------------
--- @param t
local torderedset = function(t)
local r = { }
for i = 1, #t do
local v = t[i]
-- Have to add this limitation to avoid size ambiquity.
-- If you need ordered set of numbers, use separate storage
-- for set and array parts (write make_ordered_set then).
assert(type(v) ~= "number", "can't insert number into ordered set")
r[v] = i
r[i] = v
end
return r
end
--- @param t
-- @param v
-- @return[1] false if item already exists
-- @return[2] true otherwise
local torderedset_insert = function(t, v)
-- See torderedset() for motivation
assert(type(v) ~= "number", "can't insert number into ordered set")
if not t[v] then
local i = #t + 1
t[v] = i
t[i] = v
return true
end
return false
end
--- Note this operation is really slow
-- @param t
-- @param v
-- @return[1] false if item didn't existed
-- @return[2] true otherwise
local torderedset_remove = function(t, v)
-- See torderedset() for motivation
assert(type(v) ~= "number", "can't remove number from ordered set")
local pos = t[v]
if pos then
t[v] = nil
-- TODO: Do table.remove manually then to do all in a single loop.
table_remove(t, pos)
for i = pos, #t do
t[t[i]] = i -- Update changed numbers
end
end
return false
end
--------------------------------------------------------------------------------
local twithdefaults
do
--- Handles subtables (is "deep").
-- Does not support recursive defaults tables
-- WARNING: Uses tclone()! Do not use on tables with metatables!
-- @param t
-- @param defaults
-- @local
twithdefaults = function(t, defaults)
for k, d in pairs(defaults) do
local v = t[k]
if v == nil then
if type(d) == "table" then
d = tclone(d)
end
t[k] = d
elseif type(v) == "table" and type(d) == "table" then
twithdefaults(v, d)
end
end
return t
end
end
--------------------------------------------------------------------------------
--- @param pred
-- @param t
-- @param ...
local tifilter = function(pred, t, ...)
local r = { }
for i = 1, #t do
local v = t[i]
if pred(v, ...) then
r[#r + 1] = v
end
end
return r
end
--------------------------------------------------------------------------------
--- @param value
-- @param t
local tsetof = function(value, t)
local r = { }
for k, v in pairs(t) do
r[v] = value
end
return r
end
--------------------------------------------------------------------------------
--- @param ...
local tset_many = function(...)
local r = { }
for i = 1, select("#", ...) do
for k, v in pairs((select(i, ...))) do
r[v] = true
end
end
return r
end
-- TODO: Pick a better name?
--- @param t
local tidentityset = function(t)
local r = { }
for k, v in pairs(t) do
r[v] = v
end
return r
end
--------------------------------------------------------------------------------
--- @param t
-- @param key
local timapofrecords = function(t, key)
local r = { }
for i = 1, #t do
local v = t[i]
r[assert(v[key], "missing record key field")] = v
end
return r
end
--- @param t
local tivalues = function(t)
local r = { }
for i = 1, #t do
r[#r + 1] = t[i]
end
return r
end
--------------------------------------------------------------------------------
local treadonly, treadonly_ex
do
local newindex = function()
error("attempted to change read-only table")
end
--- NOTE: Optimized to be fast at simple value indexing.
-- Slower on initialization and on table value fetching.
-- WARNING: This does not protect userdata.
-- @param value
-- @param callbacks
-- @param tostring_fn
-- @param disable_nil
-- @local
treadonly = function(value, callbacks, tostring_fn, disable_nil)
callbacks = callbacks or empty_table
if disable_nil == nil then
disable_nil = true
end
arguments(
"table", value,
"table", callbacks
)
optional_arguments(
"function", tostring_fn,
"boolean", disable_nil -- TODO: ?! Not exactly optional
)
local mt =
{
__metatable = "treadonly"; -- protect metatable
__index = function(t, k)
local v = rawget(value, k)
if is_table(v) then
-- TODO: Optimize
v = treadonly(v, callbacks, tostring_fn, disable_nil)
end
if v == nil then -- TODO: Try to use metatables
-- Note: __index does not support multiple return values in 5.1,
-- so we can not do call right here.
local fn = callbacks[k]
if fn then
return function(...) return fn(value, ...) end
end
if disable_nil then
error(
"attempted to read inexistant value at key " .. tostring(k),
2
)
end
end
return v
end;
__newindex = newindex;
}
if tostring_fn then
mt.__tostring = function() return tostring_fn(value) end
end
return setmetatable({ }, mt)
end
--- Changes to second return value are guaranteed to affect first one
-- NOTE: Optimized to be fast at simple value indexing.
-- Slower on initialization and on table value fetching.
-- WARNING: This does not protect userdata.
-- @param value
-- @param callbacks
-- @param tostring_fn
-- @param disable_nil
-- @local
treadonly_ex = function(value, ...)
local protected = treadonly(value, ...)
return protected, value
end
end
--- @param fn
-- @param t
local tmap_kv = function(fn, t)
local r = { }
for k, v in pairs(t) do
k, v = fn(k, v)
r[k] = v
end
return r
end
--- @param t
-- @param key_name
local tmapofrecordgroups = function(t, key_name)
local r = { }
for k, v in pairs(t) do
local v = t[k]
local key = assert(v[key_name], "missing required key")
local g = r[key]
if not g then
g = { }
r[key] = g
end
g[#g + 1] = v
end
return r
end
--- @param t
-- @param key_name
local timapofrecordgroups = function(t, key_name)
local r = { }
for i = 1, #t do
local v = t[i]
local key = assert(v[key_name], "missing required key")
local g = r[key]
if not g then
g = { }
r[key] = g
end
g[#g + 1] = v
end
return r
end
--- @param t
-- @param k
local tilistofrecordfields = function(t, k)
local r = { }
for i = 1, #t do
local v = t[i][k]
assert(v ~= nil, "missing required key")
r[#r + 1] = v
end
return r
end
--- @param t
-- @param n
-- @param count
-- @param random
local tipermute_inplace = function(t, n, count, random)
n = n or #t
count = count or n
random = random or math.random
for i = 1, count do
local j = random(i, n)
t[i], t[j] = t[j], t[i]
end
return t
end
--- @param t
-- @param key_name
-- @param value_name
local tkvtorecordlist = function(t, key_name, value_name)
local result = { }
for k, v in pairs(t) do
result[#result + 1] = { [key_name] = k, [value_name] = v }
end
return result
end
--- @param t
-- @param k
-- @param nextk
-- @param ...
local function tgetpath(t, k, nextk, ...)
if k == nil then
return nil
end
local v = t[k]
if not is_table(v) or nextk == nil then
return v
end
return tgetpath(v, nextk, ...)
end
-- tsetpath(tabl, "a", "b", "c", d)
-- tabl.a.b.c[d] = val
local tsetpath
do
local function impl(nargs, dest, key, ...)
if nargs == 0 then
return dest
end
if key == nil then
error("tsetpath: nil can't be a table key")
end
dest[key] = assert_is_table(
dest[key] or { },
"key `" .. tostring(key)
.. "' already exists and its value is not a table"
)
return impl(nargs - 1, dest[key], ...)
end
--- @param dest
-- @param ...
-- @local
tsetpath = function(dest, ...)
local nargs = select("#", ...)
if nargs == 0 then
return dest
end
return impl(nargs, dest, ...)
end
end
local tsetpathvalue
do
local function impl(nargs, value, dest, key, ...)
assert(nargs > 0)
if key == nil then
error("tsetpathvalue: nil can't be a table key")
end
if nargs == 1 then
dest[key] = value
return dest
end
dest[key] = assert_is_table(
dest[key] or { },
"key `" .. tostring(key)
.. "' already exists and its value is not a table"
)
return impl(nargs - 1, value, dest[key], ...)
end
--- @param value
-- @param dest
-- @param ...
-- @local
tsetpathvalue = function(value, dest, ...)
local nargs = select("#", ...)
if nargs == 0 then
return dest
end
return impl(nargs, value, dest, ...)
end
end
-- TODO: rename to tislice
--- @param t
-- @param start_i
-- @param end_i
local tslice = function(t, start_i, end_i)
local r = { }
start_i = math_max(start_i, 1)
end_i = math_min(end_i, #t)
for i = start_i, end_i do
r[i - start_i + 1] = t[i]
end
return r
end
--- @param t
-- @param ...
local tarraylisttohashlist = function(t, ...)
local r = { }
local nargs = select("#", ...)
for i = 1, #t do
local item = { }
for j = 1, nargs do
local hash = select(j, ...)
if hash ~= nil then -- ignore nil from arguments
item[hash] = t[i][j]
end
end
r[#r + 1] = item
end
return r
end
--- @param t
-- @param ...
local tarraytohash = function(t, ...)
local r = { }
local nargs = select("#", ...)
for i = 1, nargs do
local hash = select(i, ...)
if hash ~= nil then -- ignore nil from arguments
r[hash] = t[i]
end
end
return r
end
--- @param t
local tisempty = function(t)
return next(t) == nil
end
--- @param t
-- @param v
local tifindvalue_nonrecursive = function(t, v)
for i = 1, #t do
if t[i] == v then
return true
end
end
return false
end
--- @param t
local tkvlist2kvpairs = function(t)
local r = { }
for i = 1, #t, 2 do
local k, v = t[i], t[i+1]
if k ~= nil then
r[k] = v
end
end
return r
end
--- @param t
-- @param f
-- @param strict
local tfilterkeylist = function(t, f, strict)
strict = strict or false
local r = { }
for i = 1, #f do
local k = f[i]
if t[k] ~= nil then
r[k] = t[k]
elseif strict then
return nil, "Field `" .. tostring(k) .. "' is absent"
end
end
return r
end
--- @param fn
-- @param t
-- @param ...
local tkvmap_unpack = function(fn, t, ...)
local r = { }
for k, v in pairs(t) do
k, v = fn(k, ...), fn(v, ...)
if k ~= nil and v ~= nil then
r[#r + 1] = k
r[#r + 1] = v
end
end
return unpack(r)
end
--- @param t
local tkvlist_to_hash = function(t)
local r = { }
for i = 1, #t, 2 do
r[t[i]] = t[i + 1]
end
return r
end
--- @param ...
local tmerge_many = function(...)
return toverride_many({ }, ...)
end
local TISARRAY_NOT_OBJ = 'tisarray.not'
--- Makes table to be treated as JSON object in tisarray()
-- @param t
local tisarray_not = function(t)
if getmetatable(t) then
error('tisarray_not: tables with metatables are not supported')
end
return setmetatable(t, { __metatable = TISARRAY_NOT_OBJ })
end
--- Note the empty table is treated as an array
-- @return[1] true if a table is an array
-- @return[2] false otherwise
-- @param t
local tisarray = function(t)
if getmetatable(t) == TISARRAY_NOT_OBJ then
return false
end
for k, _ in pairs(t) do
if
-- Array keys should be numbers...
not is_number(k)
-- ...greater than 1...
or k < 1
-- ...in a continuous sequence...
or (k > 1 and t[k - 1] == nil)
-- ...of integers...
or k % 1 ~= 0
-- ...avoiding floating point overflow
or k == k - 1
then
return false
end
end
return true
end
--------------------------------------------------------------------------------
local tdeepfilter
do
local function impl(predicate, t, visited)
if not is_table(t) then
return t
end
local r = { }
assert(not visited[t], 'recursion detected')
visited[t] = true
for k, v in pairs(t) do
if predicate(v, k, t) then
r[impl(predicate, k, visited)] = impl(predicate, v, visited)
end
end
visited[t] = nil
return r
end
--- @param predicate
-- @param t
-- @local
tdeepfilter = function(predicate, t)
return impl(predicate, t, { })
end
end
--------------------------------------------------------------------------------
local tifindallpermutations
do
--- Finds all permutations of elements in the input linear array table `t`
-- and puts them into the `results` table.
-- @param t Input linear array table.
-- @param results All found permutations will be placed here.
-- @return None
-- @local
-- @usage
-- local tifindallpermutations
-- = import 'lua-nucleo/table-utils.lua'
-- {
-- 'tifindallpermutations'
-- }
--
-- local permutations0 = tifindallpermutations({ })
-- -- permutations0 is { } here
--
-- local permutations1 = tifindallpermutations({ 1 }, { { 1 } })
-- -- permutations1 is { { 1 } } here
--
-- local permutations2 = tifindallpermutations({ 1, 2 })
-- -- permutations2 is { { 1, 2 }, { 2, 1 } }
--
-- local permutations3 = tifindallpermutations({ 1, 2, 3 })
-- -- permutations3 is
-- -- {
-- -- { 2, 3, 1 },
-- -- { 3, 2, 1 },
-- -- { 1, 3, 2 },
-- -- { 3, 1, 2 },
-- -- { 1, 2, 3 },
-- -- { 2, 1, 3 }
-- -- }
--
-- local permutations4 = tifindallpermutations({ 1, 2, 3, 4 })
-- -- permutations4 is
-- -- {
-- -- { 3, 4, 2, 1 },
-- -- { 4, 3, 2, 1 },
-- -- { 2, 4, 3, 1 },
-- -- { 4, 2, 3, 1 },
-- -- { 2, 3, 4, 1 },
-- -- { 3, 2, 4, 1 },
-- -- { 3, 4, 1, 2 },
-- -- { 4, 3, 1, 2 },
-- -- { 1, 4, 3, 2 },
-- -- { 4, 1, 3, 2 },
-- -- { 1, 3, 4, 2 },
-- -- { 3, 1, 4, 2 },
-- -- { 2, 4, 1, 3 },
-- -- { 4, 2, 1, 3 },
-- -- { 1, 4, 2, 3 },
-- -- { 4, 1, 2, 3 },
-- -- { 1, 2, 4, 3 },
-- -- { 2, 1, 4, 3 },
-- -- { 2, 3, 1, 4 },
-- -- { 3, 2, 1, 4 },
-- -- { 1, 3, 2, 4 },
-- -- { 3, 1, 2, 4 },
-- -- { 1, 2, 3, 4 },
-- -- { 2, 1, 3, 4 }
-- -- }
tifindallpermutations = function(t, results)
arguments(
'table', t,
'table', results
)
if #t == 0 then
return
elseif #t == 1 then
results[#results + 1] = { t[1] }
return
elseif #t == 2 then
results[#results + 1] = { t[1], t[2] }
results[#results + 1] = { t[2], t[1] }
return
end
for i = 1, #t do
local rest = { }
local rest_permutations = { }
for j = 1, #t do
if j ~= i then
rest[#rest + 1] = t[j]
end
end
tifindallpermutations(rest, rest_permutations)
for j = 1, #rest_permutations do
local permutation = rest_permutations[j]
permutation[#permutation + 1] = t[i]
results[#results + 1] = permutation
end
end
end
end
--------------------------------------------------------------------------------
return
{
empty_table = empty_table;
toverride_many = toverride_many;
tappend_many = tappend_many;
tijoin_many = tijoin_many;
tkeys = tkeys;
tvalues = tvalues;
tkeysvalues = tkeysvalues;
tflip = tflip;
tflip_inplace = tflip_inplace;
tiflip = tiflip;
tset = tset;
tiset = tiset;
tisarray_not = tisarray_not;
tisarray = tisarray;
tiinsert_args = tiinsert_args;
timap_inplace = timap_inplace;
timap = timap;
timap_sliding = timap_sliding;
tiwalk = tiwalk;
tiwalker = tiwalker;
tequals = tequals;
tiunique = tiunique;
tgenerate_n = tgenerate_n; -- deprecated
tgenerate_1d_linear = tgenerate_1d_linear;
tgenerate_2d_linear = tgenerate_2d_linear;
taccumulate = taccumulate;
tnormalize = tnormalize;
tnormalize_inplace = tnormalize_inplace;
tclone = tclone;
tcount_elements = tcount_elements;
tremap_to_array = tremap_to_array;
twalk_pairs = twalk_pairs;
tmap_values = tmap_values;
torderedset = torderedset;
torderedset_insert = torderedset_insert;
torderedset_remove = torderedset_remove;
twithdefaults = twithdefaults;
tifilter = tifilter;
tsetof = tsetof;
tset_many = tset_many;
tidentityset = tidentityset;
timapofrecords = timapofrecords;
tivalues = tivalues;
treadonly = treadonly;
treadonly_ex = treadonly_ex;
tmap_kv = tmap_kv;
tmapofrecordgroups = tmapofrecordgroups;
timapofrecordgroups = timapofrecordgroups;
tilistofrecordfields = tilistofrecordfields;
tipermute_inplace = tipermute_inplace;
tkvtorecordlist = tkvtorecordlist;
tgetpath = tgetpath;
tsetpath = tsetpath;
tsetpathvalue = tsetpathvalue;
tslice = tslice;
tarraylisttohashlist = tarraylisttohashlist;
tarraytohash = tarraytohash;
tkvlist2kvpairs = tkvlist2kvpairs;
tfilterkeylist = tfilterkeylist;
tisempty = tisempty;
tifindvalue_nonrecursive = tifindvalue_nonrecursive;
tkvmap_unpack = tkvmap_unpack;
tkvlist_to_hash = tkvlist_to_hash;
tmerge_many = tmerge_many;
tdeepfilter = tdeepfilter;
tifindallpermutations = tifindallpermutations;
}