summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/penlight/pl/tablex.lua
diff options
context:
space:
mode:
Diffstat (limited to 'macros/luatex/generic/penlight/pl/tablex.lua')
-rw-r--r--macros/luatex/generic/penlight/pl/tablex.lua999
1 files changed, 0 insertions, 999 deletions
diff --git a/macros/luatex/generic/penlight/pl/tablex.lua b/macros/luatex/generic/penlight/pl/tablex.lua
deleted file mode 100644
index 81f739529e..0000000000
--- a/macros/luatex/generic/penlight/pl/tablex.lua
+++ /dev/null
@@ -1,999 +0,0 @@
---- Extended operations on Lua tables.
---
--- See @{02-arrays.md.Useful_Operations_on_Tables|the Guide}
---
--- Dependencies: `pl.utils`, `pl.types`
--- @module pl.tablex
-local utils = require ('pl.utils')
-local types = require ('pl.types')
-local getmetatable,setmetatable,require = getmetatable,setmetatable,require
-local tsort,append,remove = table.sort,table.insert,table.remove
-local min = math.min
-local pairs,type,unpack,select,tostring = pairs,type,utils.unpack,select,tostring
-local function_arg = utils.function_arg
-local assert_arg = utils.assert_arg
-
-local tablex = {}
-
--- generally, functions that make copies of tables try to preserve the metatable.
--- However, when the source has no obvious type, then we attach appropriate metatables
--- like List, Map, etc to the result.
-local function setmeta (res,tbl,pl_class)
- local mt = getmetatable(tbl) or pl_class and require('pl.' .. pl_class)
- return mt and setmetatable(res, mt) or res
-end
-
-local function makelist(l)
- return setmetatable(l, require('pl.List'))
-end
-
-local function makemap(m)
- return setmetatable(m, require('pl.Map'))
-end
-
-local function complain (idx,msg)
- error(('argument %d is not %s'):format(idx,msg),3)
-end
-
-local function assert_arg_indexable (idx,val)
- if not types.is_indexable(val) then
- complain(idx,"indexable")
- end
-end
-
-local function assert_arg_iterable (idx,val)
- if not types.is_iterable(val) then
- complain(idx,"iterable")
- end
-end
-
-local function assert_arg_writeable (idx,val)
- if not types.is_writeable(val) then
- complain(idx,"writeable")
- end
-end
-
---- copy a table into another, in-place.
--- @within Copying
--- @tab t1 destination table
--- @tab t2 source (actually any iterable object)
--- @return first table
-function tablex.update (t1,t2)
- assert_arg_writeable(1,t1)
- assert_arg_iterable(2,t2)
- for k,v in pairs(t2) do
- t1[k] = v
- end
- return t1
-end
-
---- total number of elements in this table.
--- Note that this is distinct from `#t`, which is the number
--- of values in the array part; this value will always
--- be greater or equal. The difference gives the size of
--- the hash part, for practical purposes. Works for any
--- object with a __pairs metamethod.
--- @tab t a table
--- @return the size
-function tablex.size (t)
- assert_arg_iterable(1,t)
- local i = 0
- for k in pairs(t) do i = i + 1 end
- return i
-end
-
---- make a shallow copy of a table
--- @within Copying
--- @tab t an iterable source
--- @return new table
-function tablex.copy (t)
- assert_arg_iterable(1,t)
- local res = {}
- for k,v in pairs(t) do
- res[k] = v
- end
- return res
-end
-
-local function cycle_aware_copy(t, cache)
- if type(t) ~= 'table' then return t end
- if cache[t] then return cache[t] end
- assert_arg_iterable(1,t)
- local res = {}
- cache[t] = res
- local mt = getmetatable(t)
- for k,v in pairs(t) do
- k = cycle_aware_copy(k, cache)
- v = cycle_aware_copy(v, cache)
- res[k] = v
- end
- setmetatable(res,mt)
- return res
-end
-
---- make a deep copy of a table, recursively copying all the keys and fields.
--- This supports cycles in tables; cycles will be reproduced in the copy.
--- This will also set the copied table's metatable to that of the original.
--- @within Copying
--- @tab t A table
--- @return new table
-function tablex.deepcopy(t)
- return cycle_aware_copy(t,{})
-end
-
-local abs = math.abs
-
-local function cycle_aware_compare(t1,t2,ignore_mt,eps,cache)
- if cache[t1] and cache[t1][t2] then return true end
- local ty1 = type(t1)
- local ty2 = type(t2)
- if ty1 ~= ty2 then return false end
- -- non-table types can be directly compared
- if ty1 ~= 'table' then
- if ty1 == 'number' and eps then return abs(t1-t2) < eps end
- return t1 == t2
- end
- -- as well as tables which have the metamethod __eq
- local mt = getmetatable(t1)
- if not ignore_mt and mt and mt.__eq then return t1 == t2 end
- for k1 in pairs(t1) do
- if t2[k1]==nil then return false end
- end
- for k2 in pairs(t2) do
- if t1[k2]==nil then return false end
- end
- cache[t1] = cache[t1] or {}
- cache[t1][t2] = true
- for k1,v1 in pairs(t1) do
- local v2 = t2[k1]
- if not cycle_aware_compare(v1,v2,ignore_mt,eps,cache) then return false end
- end
- return true
-end
-
---- compare two values.
--- if they are tables, then compare their keys and fields recursively.
--- @within Comparing
--- @param t1 A value
--- @param t2 A value
--- @bool[opt] ignore_mt if true, ignore __eq metamethod (default false)
--- @number[opt] eps if defined, then used for any number comparisons
--- @return true or false
-function tablex.deepcompare(t1,t2,ignore_mt,eps)
- return cycle_aware_compare(t1,t2,ignore_mt,eps,{})
-end
-
---- compare two arrays using a predicate.
--- @within Comparing
--- @array t1 an array
--- @array t2 an array
--- @func cmp A comparison function; `bool = cmp(t1_value, t2_value)`
--- @return true or false
--- @usage
--- assert(tablex.compare({ 1, 2, 3 }, { 1, 2, 3 }, "=="))
---
--- assert(tablex.compare(
--- {1,2,3, hello = "world"}, -- fields are not compared!
--- {1,2,3}, function(v1, v2) return v1 == v2 end)
-function tablex.compare (t1,t2,cmp)
- assert_arg_indexable(1,t1)
- assert_arg_indexable(2,t2)
- if #t1 ~= #t2 then return false end
- cmp = function_arg(3,cmp)
- for k = 1,#t1 do
- if not cmp(t1[k],t2[k]) then return false end
- end
- return true
-end
-
---- compare two list-like tables using an optional predicate, without regard for element order.
--- @within Comparing
--- @array t1 a list-like table
--- @array t2 a list-like table
--- @param cmp A comparison function (may be nil)
-function tablex.compare_no_order (t1,t2,cmp)
- assert_arg_indexable(1,t1)
- assert_arg_indexable(2,t2)
- if cmp then cmp = function_arg(3,cmp) end
- if #t1 ~= #t2 then return false end
- local visited = {}
- for i = 1,#t1 do
- local val = t1[i]
- local gotcha
- for j = 1,#t2 do
- if not visited[j] then
- local match
- if cmp then match = cmp(val,t2[j]) else match = val == t2[j] end
- if match then
- gotcha = j
- break
- end
- end
- end
- if not gotcha then return false end
- visited[gotcha] = true
- end
- return true
-end
-
-
---- return the index of a value in a list.
--- Like string.find, there is an optional index to start searching,
--- which can be negative.
--- @within Finding
--- @array t A list-like table
--- @param val A value
--- @int idx index to start; -1 means last element,etc (default 1)
--- @return index of value or nil if not found
--- @usage find({10,20,30},20) == 2
--- @usage find({'a','b','a','c'},'a',2) == 3
-function tablex.find(t,val,idx)
- assert_arg_indexable(1,t)
- idx = idx or 1
- if idx < 0 then idx = #t + idx + 1 end
- for i = idx,#t do
- if t[i] == val then return i end
- end
- return nil
-end
-
---- return the index of a value in a list, searching from the end.
--- Like string.find, there is an optional index to start searching,
--- which can be negative.
--- @within Finding
--- @array t A list-like table
--- @param val A value
--- @param idx index to start; -1 means last element,etc (default `#t`)
--- @return index of value or nil if not found
--- @usage rfind({10,10,10},10) == 3
-function tablex.rfind(t,val,idx)
- assert_arg_indexable(1,t)
- idx = idx or #t
- if idx < 0 then idx = #t + idx + 1 end
- for i = idx,1,-1 do
- if t[i] == val then return i end
- end
- return nil
-end
-
-
---- return the index (or key) of a value in a table using a comparison function.
---
--- *NOTE*: the 2nd return value of this function, the value returned
--- by the comparison function, has a limitation that it cannot be `false`.
--- Because if it is, then it indicates the comparison failed, and the
--- function will continue the search. See examples.
--- @within Finding
--- @tab t A table
--- @func cmp A comparison function
--- @param arg an optional second argument to the function
--- @return index of value, or nil if not found
--- @return value returned by comparison function (cannot be `false`!)
--- @usage
--- -- using an operator
--- local lst = { "Rudolph", true, false, 15 }
--- local idx, cmp_result = tablex.rfind(lst, "==", "Rudolph")
--- assert(idx == 1)
--- assert(cmp_result == true)
---
--- local idx, cmp_result = tablex.rfind(lst, "==", false)
--- assert(idx == 3)
--- assert(cmp_result == true) -- looking up 'false' works!
---
--- -- using a function returning the value looked up
--- local cmp = function(v1, v2) return v1 == v2 and v2 end
--- local idx, cmp_result = tablex.rfind(lst, cmp, "Rudolph")
--- assert(idx == 1)
--- assert(cmp_result == "Rudolph") -- the value is returned
---
--- -- NOTE: this fails, since 'false' cannot be returned!
--- local idx, cmp_result = tablex.rfind(lst, cmp, false)
--- assert(idx == nil) -- looking up 'false' failed!
--- assert(cmp_result == nil)
-function tablex.find_if(t,cmp,arg)
- assert_arg_iterable(1,t)
- cmp = function_arg(2,cmp)
- for k,v in pairs(t) do
- local c = cmp(v,arg)
- if c then return k,c end
- end
- return nil
-end
-
---- return a list of all values in a table indexed by another list.
--- @tab tbl a table
--- @array idx an index table (a list of keys)
--- @return a list-like table
--- @usage index_by({10,20,30,40},{2,4}) == {20,40}
--- @usage index_by({one=1,two=2,three=3},{'one','three'}) == {1,3}
-function tablex.index_by(tbl,idx)
- assert_arg_indexable(1,tbl)
- assert_arg_indexable(2,idx)
- local res = {}
- for i = 1,#idx do
- res[i] = tbl[idx[i]]
- end
- return setmeta(res,tbl,'List')
-end
-
---- apply a function to all values of a table.
--- This returns a table of the results.
--- Any extra arguments are passed to the function.
--- @within MappingAndFiltering
--- @func fun A function that takes at least one argument
--- @tab t A table
--- @param ... optional arguments
--- @usage map(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900,fred=4}
-function tablex.map(fun,t,...)
- assert_arg_iterable(1,t)
- fun = function_arg(1,fun)
- local res = {}
- for k,v in pairs(t) do
- res[k] = fun(v,...)
- end
- return setmeta(res,t)
-end
-
---- apply a function to all values of a list.
--- This returns a table of the results.
--- Any extra arguments are passed to the function.
--- @within MappingAndFiltering
--- @func fun A function that takes at least one argument
--- @array t a table (applies to array part)
--- @param ... optional arguments
--- @return a list-like table
--- @usage imap(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900}
-function tablex.imap(fun,t,...)
- assert_arg_indexable(1,t)
- fun = function_arg(1,fun)
- local res = {}
- for i = 1,#t do
- res[i] = fun(t[i],...) or false
- end
- return setmeta(res,t,'List')
-end
-
---- apply a named method to values from a table.
--- @within MappingAndFiltering
--- @string name the method name
--- @array t a list-like table
--- @param ... any extra arguments to the method
--- @return a `List` with the results of the method (1st result only)
--- @usage
--- local Car = {}
--- Car.__index = Car
--- function Car.new(car)
--- return setmetatable(car or {}, Car)
--- end
--- Car.speed = 0
--- function Car:faster(increase)
--- self.speed = self.speed + increase
--- return self.speed
--- end
---
--- local ferrari = Car.new{ name = "Ferrari" }
--- local lamborghini = Car.new{ name = "Lamborghini", speed = 50 }
--- local cars = { ferrari, lamborghini }
---
--- assert(ferrari.speed == 0)
--- assert(lamborghini.speed == 50)
--- tablex.map_named_method("faster", cars, 10)
--- assert(ferrari.speed == 10)
--- assert(lamborghini.speed == 60)
-function tablex.map_named_method (name,t,...)
- utils.assert_string(1,name)
- assert_arg_indexable(2,t)
- local res = {}
- for i = 1,#t do
- local val = t[i]
- local fun = val[name]
- res[i] = fun(val,...)
- end
- return setmeta(res,t,'List')
-end
-
---- apply a function to all values of a table, in-place.
--- Any extra arguments are passed to the function.
--- @func fun A function that takes at least one argument
--- @tab t a table
--- @param ... extra arguments passed to `fun`
--- @see tablex.foreach
-function tablex.transform (fun,t,...)
- assert_arg_iterable(1,t)
- fun = function_arg(1,fun)
- for k,v in pairs(t) do
- t[k] = fun(v,...)
- end
-end
-
---- generate a table of all numbers in a range.
--- This is consistent with a numerical for loop.
--- @int start number
--- @int finish number
--- @int[opt=1] step make this negative for start < finish
-function tablex.range (start,finish,step)
- local res
- step = step or 1
- if start == finish then
- res = {start}
- elseif (start > finish and step > 0) or (finish > start and step < 0) then
- res = {}
- else
- local k = 1
- res = {}
- for i=start,finish,step do res[k]=i; k=k+1 end
- end
- return makelist(res)
-end
-
---- apply a function to values from two tables.
--- @within MappingAndFiltering
--- @func fun a function of at least two arguments
--- @tab t1 a table
--- @tab t2 a table
--- @param ... extra arguments
--- @return a table
--- @usage map2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23,m=44}
-function tablex.map2 (fun,t1,t2,...)
- assert_arg_iterable(1,t1)
- assert_arg_iterable(2,t2)
- fun = function_arg(1,fun)
- local res = {}
- for k,v in pairs(t1) do
- res[k] = fun(v,t2[k],...)
- end
- return setmeta(res,t1,'List')
-end
-
---- apply a function to values from two arrays.
--- The result will be the length of the shortest array.
--- @within MappingAndFiltering
--- @func fun a function of at least two arguments
--- @array t1 a list-like table
--- @array t2 a list-like table
--- @param ... extra arguments
--- @usage imap2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23}
-function tablex.imap2 (fun,t1,t2,...)
- assert_arg_indexable(2,t1)
- assert_arg_indexable(3,t2)
- fun = function_arg(1,fun)
- local res,n = {},math.min(#t1,#t2)
- for i = 1,n do
- res[i] = fun(t1[i],t2[i],...)
- end
- return res
-end
-
---- 'reduce' a list using a binary function.
--- @func fun a function of two arguments
--- @array t a list-like table
--- @array memo optional initial memo value. Defaults to first value in table.
--- @return the result of the function
--- @usage reduce('+',{1,2,3,4}) == 10
-function tablex.reduce (fun,t,memo)
- assert_arg_indexable(2,t)
- fun = function_arg(1,fun)
- local n = #t
- if n == 0 then
- return memo
- end
- local res = memo and fun(memo, t[1]) or t[1]
- for i = 2,n do
- res = fun(res,t[i])
- end
- return res
-end
-
---- apply a function to all elements of a table.
--- The arguments to the function will be the value,
--- the key and _finally_ any extra arguments passed to this function.
--- Note that the Lua 5.0 function table.foreach passed the _key_ first.
--- @within Iterating
--- @tab t a table
--- @func fun a function on the elements; `function(value, key, ...)`
--- @param ... extra arguments passed to `fun`
--- @see tablex.transform
-function tablex.foreach(t,fun,...)
- assert_arg_iterable(1,t)
- fun = function_arg(2,fun)
- for k,v in pairs(t) do
- fun(v,k,...)
- end
-end
-
---- apply a function to all elements of a list-like table in order.
--- The arguments to the function will be the value,
--- the index and _finally_ any extra arguments passed to this function
--- @within Iterating
--- @array t a table
--- @func fun a function with at least one argument
--- @param ... optional arguments
-function tablex.foreachi(t,fun,...)
- assert_arg_indexable(1,t)
- fun = function_arg(2,fun)
- for i = 1,#t do
- fun(t[i],i,...)
- end
-end
-
---- Apply a function to a number of tables.
--- A more general version of map
--- The result is a table containing the result of applying that function to the
--- ith value of each table. Length of output list is the minimum length of all the lists
--- @within MappingAndFiltering
--- @func fun a function of n arguments
--- @tab ... n tables
--- @usage mapn(function(x,y,z) return x+y+z end, {1,2,3},{10,20,30},{100,200,300}) is {111,222,333}
--- @usage mapn(math.max, {1,20,300},{10,2,3},{100,200,100}) is {100,200,300}
--- @param fun A function that takes as many arguments as there are tables
-function tablex.mapn(fun,...)
- fun = function_arg(1,fun)
- local res = {}
- local lists = {...}
- local minn = 1e40
- for i = 1,#lists do
- minn = min(minn,#(lists[i]))
- end
- for i = 1,minn do
- local args,k = {},1
- for j = 1,#lists do
- args[k] = lists[j][i]
- k = k + 1
- end
- res[#res+1] = fun(unpack(args))
- end
- return res
-end
-
---- call the function with the key and value pairs from a table.
--- The function can return a value and a key (note the order!). If both
--- are not nil, then this pair is inserted into the result: if the key already exists, we convert the value for that
--- key into a table and append into it. If only value is not nil, then it is appended to the result.
--- @within MappingAndFiltering
--- @func fun A function which will be passed each key and value as arguments, plus any extra arguments to pairmap.
--- @tab t A table
--- @param ... optional arguments
--- @usage pairmap(function(k,v) return v end,{fred=10,bonzo=20}) is {10,20} _or_ {20,10}
--- @usage pairmap(function(k,v) return {k,v},k end,{one=1,two=2}) is {one={'one',1},two={'two',2}}
-function tablex.pairmap(fun,t,...)
- assert_arg_iterable(1,t)
- fun = function_arg(1,fun)
- local res = {}
- for k,v in pairs(t) do
- local rv,rk = fun(k,v,...)
- if rk then
- if res[rk] then
- if type(res[rk]) == 'table' then
- table.insert(res[rk],rv)
- else
- res[rk] = {res[rk], rv}
- end
- else
- res[rk] = rv
- end
- else
- res[#res+1] = rv
- end
- end
- return res
-end
-
-local function keys_op(i,v) return i end
-
---- return all the keys of a table in arbitrary order.
--- @within Extraction
--- @tab t A table
-function tablex.keys(t)
- assert_arg_iterable(1,t)
- return makelist(tablex.pairmap(keys_op,t))
-end
-
-local function values_op(i,v) return v end
-
---- return all the values of the table in arbitrary order
--- @within Extraction
--- @tab t A table
-function tablex.values(t)
- assert_arg_iterable(1,t)
- return makelist(tablex.pairmap(values_op,t))
-end
-
-local function index_map_op (i,v) return i,v end
-
---- create an index map from a list-like table. The original values become keys,
--- and the associated values are the indices into the original list.
--- @array t a list-like table
--- @return a map-like table
-function tablex.index_map (t)
- assert_arg_indexable(1,t)
- return makemap(tablex.pairmap(index_map_op,t))
-end
-
-local function set_op(i,v) return true,v end
-
---- create a set from a list-like table. A set is a table where the original values
--- become keys, and the associated values are all true.
--- @array t a list-like table
--- @return a set (a map-like table)
-function tablex.makeset (t)
- assert_arg_indexable(1,t)
- return setmetatable(tablex.pairmap(set_op,t),require('pl.Set'))
-end
-
---- combine two tables, either as union or intersection. Corresponds to
--- set operations for sets () but more general. Not particularly
--- useful for list-like tables.
--- @within Merging
--- @tab t1 a table
--- @tab t2 a table
--- @bool dup true for a union, false for an intersection.
--- @usage merge({alice=23,fred=34},{bob=25,fred=34}) is {fred=34}
--- @usage merge({alice=23,fred=34},{bob=25,fred=34},true) is {bob=25,fred=34,alice=23}
--- @see tablex.index_map
-function tablex.merge (t1,t2,dup)
- assert_arg_iterable(1,t1)
- assert_arg_iterable(2,t2)
- local res = {}
- for k,v in pairs(t1) do
- if dup or t2[k] then res[k] = v end
- end
- if dup then
- for k,v in pairs(t2) do
- res[k] = v
- end
- end
- return setmeta(res,t1,'Map')
-end
-
---- the union of two map-like tables.
--- If there are duplicate keys, the second table wins.
--- @tab t1 a table
--- @tab t2 a table
--- @treturn tab
--- @see tablex.merge
-function tablex.union(t1, t2)
- return tablex.merge(t1, t2, true)
-end
-
---- the intersection of two map-like tables.
--- @tab t1 a table
--- @tab t2 a table
--- @treturn tab
--- @see tablex.merge
-function tablex.intersection(t1, t2)
- return tablex.merge(t1, t2, false)
-end
-
---- a new table which is the difference of two tables.
--- With sets (where the values are all true) this is set difference and
--- symmetric difference depending on the third parameter.
--- @within Merging
--- @tab s1 a map-like table or set
--- @tab s2 a map-like table or set
--- @bool symm symmetric difference (default false)
--- @return a map-like table or set
-function tablex.difference (s1,s2,symm)
- assert_arg_iterable(1,s1)
- assert_arg_iterable(2,s2)
- local res = {}
- for k,v in pairs(s1) do
- if s2[k] == nil then res[k] = v end
- end
- if symm then
- for k,v in pairs(s2) do
- if s1[k] == nil then res[k] = v end
- end
- end
- return setmeta(res,s1,'Map')
-end
-
---- A table where the key/values are the values and value counts of the table.
--- @array t a list-like table
--- @func cmp a function that defines equality (otherwise uses ==)
--- @return a map-like table
--- @see seq.count_map
-function tablex.count_map (t,cmp)
- assert_arg_indexable(1,t)
- local res,mask = {},{}
- cmp = function_arg(2,cmp or '==')
- local n = #t
- for i = 1,#t do
- local v = t[i]
- if not mask[v] then
- mask[v] = true
- -- check this value against all other values
- res[v] = 1 -- there's at least one instance
- for j = i+1,n do
- local w = t[j]
- local ok = cmp(v,w)
- if ok then
- res[v] = res[v] + 1
- mask[w] = true
- end
- end
- end
- end
- return makemap(res)
-end
-
---- filter an array's values using a predicate function
--- @within MappingAndFiltering
--- @array t a list-like table
--- @func pred a boolean function
--- @param arg optional argument to be passed as second argument of the predicate
-function tablex.filter (t,pred,arg)
- assert_arg_indexable(1,t)
- pred = function_arg(2,pred)
- local res,k = {},1
- for i = 1,#t do
- local v = t[i]
- if pred(v,arg) then
- res[k] = v
- k = k + 1
- end
- end
- return setmeta(res,t,'List')
-end
-
---- return a table where each element is a table of the ith values of an arbitrary
--- number of tables. It is equivalent to a matrix transpose.
--- @within Merging
--- @usage zip({10,20,30},{100,200,300}) is {{10,100},{20,200},{30,300}}
--- @array ... arrays to be zipped
-function tablex.zip(...)
- return tablex.mapn(function(...) return {...} end,...)
-end
-
-local _copy
-function _copy (dest,src,idest,isrc,nsrc,clean_tail)
- idest = idest or 1
- isrc = isrc or 1
- local iend
- if not nsrc then
- nsrc = #src
- iend = #src
- else
- iend = isrc + min(nsrc-1,#src-isrc)
- end
- if dest == src then -- special case
- if idest > isrc and iend >= idest then -- overlapping ranges
- src = tablex.sub(src,isrc,nsrc)
- isrc = 1; iend = #src
- end
- end
- for i = isrc,iend do
- dest[idest] = src[i]
- idest = idest + 1
- end
- if clean_tail then
- tablex.clear(dest,idest)
- end
- return dest
-end
-
---- copy an array into another one, clearing `dest` after `idest+nsrc`, if necessary.
--- @within Copying
--- @array dest a list-like table
--- @array src a list-like table
--- @int[opt=1] idest where to start copying values into destination
--- @int[opt=1] isrc where to start copying values from source
--- @int[opt=#src] nsrc number of elements to copy from source
-function tablex.icopy (dest,src,idest,isrc,nsrc)
- assert_arg_indexable(1,dest)
- assert_arg_indexable(2,src)
- return _copy(dest,src,idest,isrc,nsrc,true)
-end
-
---- copy an array into another one.
--- @within Copying
--- @array dest a list-like table
--- @array src a list-like table
--- @int[opt=1] idest where to start copying values into destination
--- @int[opt=1] isrc where to start copying values from source
--- @int[opt=#src] nsrc number of elements to copy from source
-function tablex.move (dest,src,idest,isrc,nsrc)
- assert_arg_indexable(1,dest)
- assert_arg_indexable(2,src)
- return _copy(dest,src,idest,isrc,nsrc,false)
-end
-
-function tablex._normalize_slice(self,first,last)
- local sz = #self
- if not first then first=1 end
- if first<0 then first=sz+first+1 end
- -- make the range _inclusive_!
- if not last then last=sz end
- if last < 0 then last=sz+1+last end
- return first,last
-end
-
---- Extract a range from a table, like 'string.sub'.
--- If first or last are negative then they are relative to the end of the list
--- eg. sub(t,-2) gives last 2 entries in a list, and
--- sub(t,-4,-2) gives from -4th to -2nd
--- @within Extraction
--- @array t a list-like table
--- @int first An index
--- @int last An index
--- @return a new List
-function tablex.sub(t,first,last)
- assert_arg_indexable(1,t)
- first,last = tablex._normalize_slice(t,first,last)
- local res={}
- for i=first,last do append(res,t[i]) end
- return setmeta(res,t,'List')
-end
-
---- set an array range to a value. If it's a function we use the result
--- of applying it to the indices.
--- @array t a list-like table
--- @param val a value
--- @int[opt=1] i1 start range
--- @int[opt=#t] i2 end range
-function tablex.set (t,val,i1,i2)
- assert_arg_indexable(1,t)
- i1,i2 = i1 or 1,i2 or #t
- if types.is_callable(val) then
- for i = i1,i2 do
- t[i] = val(i)
- end
- else
- for i = i1,i2 do
- t[i] = val
- end
- end
-end
-
---- create a new array of specified size with initial value.
--- @int n size
--- @param val initial value (can be `nil`, but don't expect `#` to work!)
--- @return the table
-function tablex.new (n,val)
- local res = {}
- tablex.set(res,val,1,n)
- return res
-end
-
---- clear out the contents of a table.
--- @array t a list
--- @param istart optional start position
-function tablex.clear(t,istart)
- istart = istart or 1
- for i = istart,#t do remove(t) end
-end
-
---- insert values into a table.
--- similar to `table.insert` but inserts values from given table `values`,
--- not the object itself, into table `t` at position `pos`.
--- @within Copying
--- @array t the list
--- @int[opt] position (default is at end)
--- @array values
-function tablex.insertvalues(t, ...)
- assert_arg(1,t,'table')
- local pos, values
- if select('#', ...) == 1 then
- pos,values = #t+1, ...
- else
- pos,values = ...
- end
- if #values > 0 then
- for i=#t,pos,-1 do
- t[i+#values] = t[i]
- end
- local offset = 1 - pos
- for i=pos,pos+#values-1 do
- t[i] = values[i + offset]
- end
- end
- return t
-end
-
---- remove a range of values from a table.
--- End of range may be negative.
--- @array t a list-like table
--- @int i1 start index
--- @int i2 end index
--- @return the table
-function tablex.removevalues (t,i1,i2)
- assert_arg(1,t,'table')
- i1,i2 = tablex._normalize_slice(t,i1,i2)
- for i = i1,i2 do
- remove(t,i1)
- end
- return t
-end
-
-local _find
-_find = function (t,value,tables)
- for k,v in pairs(t) do
- if v == value then return k end
- end
- for k,v in pairs(t) do
- if not tables[v] and type(v) == 'table' then
- tables[v] = true
- local res = _find(v,value,tables)
- if res then
- res = tostring(res)
- if type(k) ~= 'string' then
- return '['..k..']'..res
- else
- return k..'.'..res
- end
- end
- end
- end
-end
-
---- find a value in a table by recursive search.
--- @within Finding
--- @tab t the table
--- @param value the value
--- @array[opt] exclude any tables to avoid searching
--- @return a fieldspec, e.g. 'a.b' or 'math.sin'
--- @usage search(_G,math.sin,{package.path}) == 'math.sin'
-function tablex.search (t,value,exclude)
- assert_arg_iterable(1,t)
- local tables = {[t]=true}
- if exclude then
- for _,v in pairs(exclude) do tables[v] = true end
- end
- return _find(t,value,tables)
-end
-
---- return an iterator to a table sorted by its keys
--- @within Iterating
--- @tab t the table
--- @func f an optional comparison function (f(x,y) is true if x < y)
--- @usage for k,v in tablex.sort(t) do print(k,v) end
--- @return an iterator to traverse elements sorted by the keys
-function tablex.sort(t,f)
- local keys = {}
- for k in pairs(t) do keys[#keys + 1] = k end
- tsort(keys,f)
- local i = 0
- return function()
- i = i + 1
- return keys[i], t[keys[i]]
- end
-end
-
---- return an iterator to a table sorted by its values
--- @within Iterating
--- @tab t the table
--- @func f an optional comparison function (f(x,y) is true if x < y)
--- @usage for k,v in tablex.sortv(t) do print(k,v) end
--- @return an iterator to traverse elements sorted by the values
-function tablex.sortv(t,f)
- f = function_arg(2, f or '<')
- local keys = {}
- for k in pairs(t) do keys[#keys + 1] = k end
- tsort(keys,function(x, y) return f(t[x], t[y]) end)
- local i = 0
- return function()
- i = i + 1
- return keys[i], t[keys[i]]
- end
-end
-
---- modifies a table to be read only.
--- This only offers weak protection. Tables can still be modified with
--- `table.insert` and `rawset`.
---
--- *NOTE*: for Lua 5.1 length, pairs and ipairs will not work, since the
--- equivalent metamethods are only available in Lua 5.2 and newer.
--- @tab t the table
--- @return the table read only (a proxy).
-function tablex.readonly(t)
- local mt = {
- __index=t,
- __newindex=function(t, k, v) error("Attempt to modify read-only table", 2) end,
- __pairs=function() return pairs(t) end,
- __ipairs=function() return ipairs(t) end,
- __len=function() return #t end,
- __metatable=false
- }
- return setmetatable({}, mt)
-end
-
-return tablex