• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lunarmodules / Penlight / 583

11 Aug 2025 02:27PM UTC coverage: 89.269% (+0.4%) from 88.871%
583

Pull #498

appveyor

web-flow
fix(*): some more Lua 5.5 fixes for constant loop variables (#499)
Pull Request #498: fix(*): some Lua 5.5 fixes for constant loop variables

23 of 24 new or added lines in 4 files covered. (95.83%)

79 existing lines in 14 files now uncovered.

5482 of 6141 relevant lines covered (89.27%)

165.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.79
/lua/pl/tablex.lua
1
--- Extended operations on Lua tables.
2
--
3
-- See @{02-arrays.md.Useful_Operations_on_Tables|the Guide}
4
--
5
-- Dependencies: `pl.utils`, `pl.types`
6
-- @module pl.tablex
7
local utils = require ('pl.utils')
140✔
8
local types = require ('pl.types')
140✔
9
local getmetatable,setmetatable,require = getmetatable,setmetatable,require
140✔
10
local tsort,append,remove = table.sort,table.insert,table.remove
140✔
11
local min = math.min
140✔
12
local pairs,type,unpack,select,tostring = pairs,type,utils.unpack,select,tostring
140✔
13
local function_arg = utils.function_arg
140✔
14
local assert_arg = utils.assert_arg
140✔
15

16
local tablex = {}
140✔
17

18
-- generally, functions that make copies of tables try to preserve the metatable.
19
-- However, when the source has no obvious type, then we attach appropriate metatables
20
-- like List, Map, etc to the result.
21
local function setmeta (res,tbl,pl_class)
22
    local mt = getmetatable(tbl) or pl_class and require('pl.' .. pl_class)
1,000✔
23
    return mt and setmetatable(res, mt) or res
1,000✔
24
end
25

26
local function makelist(l)
27
    return setmetatable(l, require('pl.List'))
28✔
28
end
29

30
local function makemap(m)
31
    return setmetatable(m, require('pl.Map'))
12✔
32
end
33

34
local function complain (idx,msg)
35
    error(('argument %d is not %s'):format(idx,msg),3)
×
36
end
37

38
local function assert_arg_indexable (idx,val)
39
    if not types.is_indexable(val) then
700✔
40
        complain(idx,"indexable")
×
41
    end
42
end
43

44
local function assert_arg_iterable (idx,val)
45
    if not types.is_iterable(val) then
1,116✔
46
        complain(idx,"iterable")
×
47
    end
48
end
49

50
local function assert_arg_writeable (idx,val)
51
    if not types.is_writeable(val) then
8✔
52
        complain(idx,"writeable")
×
53
    end
54
end
55

56
--- copy a table into another, in-place.
57
-- @within Copying
58
-- @tab t1 destination table
59
-- @tab t2 source (actually any iterable object)
60
-- @return first table
61
function tablex.update (t1,t2)
140✔
62
    assert_arg_writeable(1,t1)
8✔
63
    assert_arg_iterable(2,t2)
8✔
64
    for k,v in pairs(t2) do
36✔
65
        t1[k] = v
28✔
66
    end
67
    return t1
8✔
68
end
69

70
--- total number of elements in this table.
71
-- Note that this is distinct from `#t`, which is the number
72
-- of values in the array part; this value will always
73
-- be greater or equal. The difference gives the size of
74
-- the hash part, for practical purposes. Works for any
75
-- object with a __pairs metamethod.
76
-- @tab t a table
77
-- @return the size
78
function tablex.size (t)
140✔
79
    assert_arg_iterable(1,t)
12✔
80
    local i = 0
12✔
81
    for k in pairs(t) do i = i + 1 end
28✔
82
    return i
12✔
83
end
84

85
--- make a shallow copy of a table
86
-- @within Copying
87
-- @tab t an iterable source
88
-- @return new table
89
function tablex.copy (t)
140✔
90
    assert_arg_iterable(1,t)
108✔
91
    local res = {}
108✔
92
    for k,v in pairs(t) do
5,600✔
93
        res[k] = v
5,492✔
94
    end
95
    return res
108✔
96
end
97

98
local function cycle_aware_copy(t, cache)
99
    if type(t) ~= 'table' then return t end
248✔
100
    if cache[t] then return cache[t] end
44✔
101
    assert_arg_iterable(1,t)
40✔
102
    local res = {}
40✔
103
    cache[t] = res
40✔
104
    local mt = getmetatable(t)
40✔
105
    for k,v in pairs(t) do
148✔
106
        res[cycle_aware_copy(k, cache)] = cycle_aware_copy(v, cache)
108✔
107
    end
108
    setmetatable(res,mt)
40✔
109
    return res
40✔
110
end
111

112
--- make a deep copy of a table, recursively copying all the keys and fields.
113
-- This supports cycles in tables; cycles will be reproduced in the copy.
114
-- This will also set the copied table's metatable to that of the original.
115
-- @within Copying
116
-- @tab t A table
117
-- @return new table
118
function tablex.deepcopy(t)
140✔
119
    return cycle_aware_copy(t,{})
32✔
120
end
121

122
local abs = math.abs
140✔
123

124
local function cycle_aware_compare(t1,t2,ignore_mt,eps,cache)
125
    if cache[t1] and cache[t1][t2] then return true end
5,492✔
126
    local ty1 = type(t1)
5,488✔
127
    local ty2 = type(t2)
5,488✔
128
    if ty1 ~= ty2 then return false end
5,488✔
129
    -- non-table types can be directly compared
130
    if ty1 ~= 'table' then
5,488✔
131
        if ty1 == 'number' and eps then return abs(t1-t2) < eps end
3,764✔
132
        return t1 == t2
3,626✔
133
    end
134
    -- as well as tables which have the metamethod __eq
135
    local mt = getmetatable(t1)
1,724✔
136
    if not ignore_mt and mt and mt.__eq then return t1 == t2 end
1,724✔
137
    for k1 in pairs(t1) do
6,182✔
138
        if t2[k1]==nil then return false end
4,458✔
139
    end
140
    for k2 in pairs(t2) do
6,182✔
141
        if t1[k2]==nil then return false end
4,458✔
142
    end
143
    cache[t1] = cache[t1] or {}
1,724✔
144
    cache[t1][t2] = true
1,724✔
145
    for k1,v1 in pairs(t1) do
6,182✔
146
        local v2 = t2[k1]
4,458✔
147
        if not cycle_aware_compare(v1,v2,ignore_mt,eps,cache) then return false end
4,458✔
148
    end
149
    return true
1,724✔
150
end
151

152
--- compare two values.
153
-- if they are tables, then compare their keys and fields recursively.
154
-- @within Comparing
155
-- @param t1 A value
156
-- @param t2 A value
157
-- @bool[opt] ignore_mt if true, ignore __eq metamethod (default false)
158
-- @number[opt] eps if defined, then used for any number comparisons
159
-- @return true or false
160
function tablex.deepcompare(t1,t2,ignore_mt,eps)
140✔
161
    return cycle_aware_compare(t1,t2,ignore_mt,eps,{})
1,034✔
162
end
163

164
--- compare two arrays using a predicate.
165
-- @within Comparing
166
-- @array t1 an array
167
-- @array t2 an array
168
-- @func cmp A comparison function; `bool = cmp(t1_value, t2_value)`
169
-- @return true or false
170
-- @usage
171
-- assert(tablex.compare({ 1, 2, 3 }, { 1, 2, 3 }, "=="))
172
--
173
-- assert(tablex.compare(
174
--    {1,2,3, hello = "world"},  -- fields are not compared!
175
--    {1,2,3}, function(v1, v2) return v1 == v2 end)
176
function tablex.compare (t1,t2,cmp)
140✔
177
    assert_arg_indexable(1,t1)
16✔
178
    assert_arg_indexable(2,t2)
16✔
179
    if #t1 ~= #t2 then return false end
16✔
180
    cmp = function_arg(3,cmp)
16✔
181
    for k = 1,#t1 do
44✔
182
        if not cmp(t1[k],t2[k]) then return false end
32✔
183
    end
184
    return true
12✔
185
end
186

187
--- compare two list-like tables using an optional predicate, without regard for element order.
188
-- @within Comparing
189
-- @array t1 a list-like table
190
-- @array t2 a list-like table
191
-- @param cmp A comparison function (may be nil)
192
function tablex.compare_no_order (t1,t2,cmp)
140✔
193
    assert_arg_indexable(1,t1)
28✔
194
    assert_arg_indexable(2,t2)
28✔
195
    if cmp then cmp = function_arg(3,cmp) end
28✔
196
    if #t1 ~= #t2 then return false end
28✔
197
    local visited = {}
28✔
198
    for i = 1,#t1 do
104✔
199
        local val = t1[i]
80✔
200
        local gotcha
201
        for j = 1,#t2 do
168✔
202
            if not visited[j] then
164✔
203
                local match
204
                if cmp then match = cmp(val,t2[j]) else match = val == t2[j] end
108✔
205
                if match then
108✔
206
                    gotcha = j
76✔
207
                    break
38✔
208
                end
209
            end
210
        end
211
        if not gotcha then return false end
80✔
212
        visited[gotcha] = true
76✔
213
    end
214
    return true
24✔
215
end
216

217

218
--- return the index of a value in a list.
219
-- Like string.find, there is an optional index to start searching,
220
-- which can be negative.
221
-- @within Finding
222
-- @array t A list-like table
223
-- @param val A value
224
-- @int idx index to start; -1 means last element,etc (default 1)
225
-- @return index of value or nil if not found
226
-- @usage find({10,20,30},20) == 2
227
-- @usage find({'a','b','a','c'},'a',2) == 3
228
function tablex.find(t,val,idx)
140✔
229
    assert_arg_indexable(1,t)
20✔
230
    idx = idx or 1
20✔
231
    if idx < 0 then idx = #t + idx + 1 end
20✔
232
    for i = idx,#t do
92✔
233
        if t[i] == val then return i end
88✔
234
    end
235
    return nil
4✔
236
end
237

238
--- return the index of a value in a list, searching from the end.
239
-- Like string.find, there is an optional index to start searching,
240
-- which can be negative.
241
-- @within Finding
242
-- @array t A list-like table
243
-- @param val A value
244
-- @param idx index to start; -1 means last element,etc (default `#t`)
245
-- @return index of value or nil if not found
246
-- @usage rfind({10,10,10},10) == 3
247
function tablex.rfind(t,val,idx)
140✔
248
    assert_arg_indexable(1,t)
28✔
249
    idx = idx or #t
28✔
250
    if idx < 0 then idx = #t + idx + 1 end
28✔
251
    for i = idx,1,-1 do
68✔
252
        if t[i] == val then return i end
56✔
253
    end
254
    return nil
12✔
255
end
256

257

258
--- return the index (or key) of a value in a table using a comparison function.
259
--
260
-- *NOTE*: the 2nd return value of this function, the value returned
261
-- by the comparison function, has a limitation that it cannot be `false`.
262
-- Because if it is, then it indicates the comparison failed, and the
263
-- function will continue the search. See examples.
264
-- @within Finding
265
-- @tab t A table
266
-- @func cmp A comparison function
267
-- @param arg an optional second argument to the function
268
-- @return index of value, or nil if not found
269
-- @return value returned by comparison function (cannot be `false`!)
270
-- @usage
271
-- -- using an operator
272
-- local lst = { "Rudolph", true, false, 15 }
273
-- local idx, cmp_result = tablex.rfind(lst, "==", "Rudolph")
274
-- assert(idx == 1)
275
-- assert(cmp_result == true)
276
--
277
-- local idx, cmp_result = tablex.rfind(lst, "==", false)
278
-- assert(idx == 3)
279
-- assert(cmp_result == true)       -- looking up 'false' works!
280
--
281
-- -- using a function returning the value looked up
282
-- local cmp = function(v1, v2) return v1 == v2 and v2 end
283
-- local idx, cmp_result = tablex.rfind(lst, cmp, "Rudolph")
284
-- assert(idx == 1)
285
-- assert(cmp_result == "Rudolph")  -- the value is returned
286
--
287
-- -- NOTE: this fails, since 'false' cannot be returned!
288
-- local idx, cmp_result = tablex.rfind(lst, cmp, false)
289
-- assert(idx == nil)               -- looking up 'false' failed!
290
-- assert(cmp_result == nil)
291
function tablex.find_if(t,cmp,arg)
140✔
292
    assert_arg_iterable(1,t)
32✔
293
    cmp = function_arg(2,cmp)
32✔
294
    for k,v in pairs(t) do
88✔
295
        local c = cmp(v,arg)
84✔
296
        if c then return k,c end
84✔
297
    end
298
    return nil
4✔
299
end
300

301
--- return a list of all values in a table indexed by another list.
302
-- @tab tbl a table
303
-- @array idx an index table (a list of keys)
304
-- @return a list-like table
305
-- @usage index_by({10,20,30,40},{2,4}) == {20,40}
306
-- @usage index_by({one=1,two=2,three=3},{'one','three'}) == {1,3}
307
function tablex.index_by(tbl,idx)
140✔
308
    assert_arg_indexable(1,tbl)
40✔
309
    assert_arg_indexable(2,idx)
40✔
310
    local res = {}
40✔
311
    for i = 1,#idx do
132✔
312
        res[i] = tbl[idx[i]]
92✔
313
    end
314
    return setmeta(res,tbl,'List')
40✔
315
end
316

317
--- apply a function to all values of a table.
318
-- This returns a table of the results.
319
-- Any extra arguments are passed to the function.
320
-- @within MappingAndFiltering
321
-- @func fun A function that takes at least one argument
322
-- @tab t A table
323
-- @param ... optional arguments
324
-- @usage map(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900,fred=4}
325
function tablex.map(fun,t,...)
140✔
326
    assert_arg_iterable(1,t)
640✔
327
    fun = function_arg(1,fun)
640✔
328
    local res = {}
640✔
329
    for k,v in pairs(t) do
2,372✔
330
        res[k] = fun(v,...)
1,732✔
331
    end
332
    return setmeta(res,t)
640✔
333
end
334

335
--- apply a function to all values of a list.
336
-- This returns a table of the results.
337
-- Any extra arguments are passed to the function.
338
-- @within MappingAndFiltering
339
-- @func fun A function that takes at least one argument
340
-- @array t a table (applies to array part)
341
-- @param ... optional arguments
342
-- @return a list-like table
343
-- @usage imap(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900}
344
function tablex.imap(fun,t,...)
140✔
345
    assert_arg_indexable(1,t)
184✔
346
    fun = function_arg(1,fun)
184✔
347
    local res = {}
184✔
348
    for i = 1,#t do
1,100✔
349
        res[i] = fun(t[i],...) or false
916✔
350
    end
351
    return setmeta(res,t,'List')
184✔
352
end
353

354
--- apply a named method to values from a table.
355
-- @within MappingAndFiltering
356
-- @string name the method name
357
-- @array t a list-like table
358
-- @param ... any extra arguments to the method
359
-- @return a `List` with the results of the method (1st result only)
360
-- @usage
361
-- local Car = {}
362
-- Car.__index = Car
363
-- function Car.new(car)
364
--   return setmetatable(car or {}, Car)
365
-- end
366
-- Car.speed = 0
367
-- function Car:faster(increase)
368
--   self.speed = self.speed + increase
369
--   return self.speed
370
-- end
371
--
372
-- local ferrari = Car.new{ name = "Ferrari" }
373
-- local lamborghini = Car.new{ name = "Lamborghini", speed = 50 }
374
-- local cars = { ferrari, lamborghini }
375
--
376
-- assert(ferrari.speed == 0)
377
-- assert(lamborghini.speed == 50)
378
-- tablex.map_named_method("faster", cars, 10)
379
-- assert(ferrari.speed == 10)
380
-- assert(lamborghini.speed == 60)
381
function tablex.map_named_method (name,t,...)
140✔
382
    utils.assert_string(1,name)
4✔
383
    assert_arg_indexable(2,t)
4✔
384
    local res = {}
4✔
385
    for i = 1,#t do
12✔
386
        local val = t[i]
8✔
387
        local fun = val[name]
8✔
388
        res[i] = fun(val,...)
8✔
389
    end
390
    return setmeta(res,t,'List')
4✔
391
end
392

393
--- apply a function to all values of a table, in-place.
394
-- Any extra arguments are passed to the function.
395
-- @func fun A function that takes at least one argument
396
-- @tab t a table
397
-- @param ... extra arguments passed to `fun`
398
-- @see tablex.foreach
399
function tablex.transform (fun,t,...)
140✔
400
    assert_arg_iterable(1,t)
8✔
401
    fun = function_arg(1,fun)
8✔
402
    for k,v in pairs(t) do
28✔
403
        t[k] = fun(v,...)
20✔
404
    end
405
end
406

407
--- generate a table of all numbers in a range.
408
-- This is consistent with a numerical for loop.
409
-- @int start  number
410
-- @int finish number
411
-- @int[opt=1] step  make this negative for start < finish
412
function tablex.range (start,finish,step)
140✔
413
    local res
414
    step = step or 1
12✔
415
    if start == finish then
12✔
416
        res = {start}
4✔
417
    elseif (start > finish and step > 0) or (finish > start and step < 0) then
8✔
418
        res = {}
4✔
419
    else
420
        local k = 1
4✔
421
        res = {}
4✔
422
        for i=start,finish,step do res[k]=i; k=k+1 end
11✔
423
    end
424
    return makelist(res)
12✔
425
end
426

427
--- apply a function to values from two tables.
428
-- @within MappingAndFiltering
429
-- @func fun a function of at least two arguments
430
-- @tab t1 a table
431
-- @tab t2 a table
432
-- @param ... extra arguments
433
-- @return a table
434
-- @usage map2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23,m=44}
435
function tablex.map2 (fun,t1,t2,...)
140✔
436
    assert_arg_iterable(1,t1)
40✔
437
    assert_arg_iterable(2,t2)
40✔
438
    fun = function_arg(1,fun)
40✔
439
    local res = {}
40✔
440
    for k,v in pairs(t1) do
124✔
441
        res[k] = fun(v,t2[k],...)
84✔
442
    end
443
    return setmeta(res,t1,'List')
40✔
444
end
445

446
--- apply a function to values from two arrays.
447
-- The result will be the length of the shortest array.
448
-- @within MappingAndFiltering
449
-- @func fun a function of at least two arguments
450
-- @array t1 a list-like table
451
-- @array t2 a list-like table
452
-- @param ... extra arguments
453
-- @usage imap2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23}
454
function tablex.imap2 (fun,t1,t2,...)
140✔
455
    assert_arg_indexable(2,t1)
20✔
456
    assert_arg_indexable(3,t2)
20✔
457
    fun = function_arg(1,fun)
20✔
458
    local res,n = {},math.min(#t1,#t2)
20✔
459
    for i = 1,n do
64✔
460
        res[i] = fun(t1[i],t2[i],...)
44✔
461
    end
462
    return res
20✔
463
end
464

465
--- 'reduce' a list using a binary function.
466
-- @func fun a function of two arguments
467
-- @array t a list-like table
468
-- @array memo optional initial memo value. Defaults to first value in table.
469
-- @return the result of the function
470
-- @usage reduce('+',{1,2,3,4}) == 10
471
function tablex.reduce (fun,t,memo)
140✔
472
    assert_arg_indexable(2,t)
92✔
473
    fun = function_arg(1,fun)
92✔
474
    local n = #t
92✔
475
    if n == 0 then
92✔
476
        return memo
8✔
477
    end
478
    local res = memo and fun(memo, t[1]) or t[1]
84✔
479
    for i = 2,n do
292✔
480
        res = fun(res,t[i])
208✔
481
    end
482
    return res
84✔
483
end
484

485
--- apply a function to all elements of a table.
486
-- The arguments to the function will be the value,
487
-- the key and _finally_ any extra arguments passed to this function.
488
-- Note that the Lua 5.0 function table.foreach passed the _key_ first.
489
-- @within Iterating
490
-- @tab t a table
491
-- @func fun a function on the elements; `function(value, key, ...)`
492
-- @param ... extra arguments passed to `fun`
493
-- @see tablex.transform
494
function tablex.foreach(t,fun,...)
140✔
495
    assert_arg_iterable(1,t)
4✔
496
    fun = function_arg(2,fun)
4✔
497
    for k,v in pairs(t) do
20✔
498
        fun(v,k,...)
16✔
499
    end
500
end
501

502
--- apply a function to all elements of a list-like table in order.
503
-- The arguments to the function will be the value,
504
-- the index and _finally_ any extra arguments passed to this function
505
-- @within Iterating
506
-- @array t a table
507
-- @func fun a function with at least one argument
508
-- @param ... optional arguments
509
function tablex.foreachi(t,fun,...)
140✔
510
    assert_arg_indexable(1,t)
4✔
511
    fun = function_arg(2,fun)
4✔
512
    for i = 1,#t do
16✔
513
        fun(t[i],i,...)
12✔
514
    end
515
end
516

517
--- Apply a function to a number of tables.
518
-- A more general version of map
519
-- The result is a table containing the result of applying that function to the
520
-- ith value of each table. Length of output list is the minimum length of all the lists
521
-- @within MappingAndFiltering
522
-- @func fun a function of n arguments
523
-- @tab ... n tables
524
-- @usage mapn(function(x,y,z) return x+y+z end, {1,2,3},{10,20,30},{100,200,300}) is {111,222,333}
525
-- @usage mapn(math.max, {1,20,300},{10,2,3},{100,200,100}) is    {100,200,300}
526
-- @param fun A function that takes as many arguments as there are tables
527
function tablex.mapn(fun,...)
140✔
528
    fun = function_arg(1,fun)
12✔
529
    local res = {}
12✔
530
    local lists = {...}
12✔
531
    local minn = 1e40
12✔
532
    for i = 1,#lists do
44✔
533
        minn = min(minn,#(lists[i]))
32✔
534
    end
535
    for i = 1,minn do
48✔
536
        local args,k = {},1
36✔
537
        for j = 1,#lists do
132✔
538
            args[k] = lists[j][i]
96✔
539
            k = k + 1
96✔
540
        end
541
        res[#res+1] = fun(unpack(args))
36✔
542
    end
543
    return res
12✔
544
end
545

546
--- call the function with the key and value pairs from a table.
547
-- The function can return a value and a key (note the order!). If both
548
-- are not nil, then this pair is inserted into the result: if the key already exists, we convert the value for that
549
-- key into a table and append into it. If only value is not nil, then it is appended to the result.
550
-- @within MappingAndFiltering
551
-- @func fun A function which will be passed each key and value as arguments, plus any extra arguments to pairmap.
552
-- @tab t A table
553
-- @param ... optional arguments
554
-- @usage pairmap(function(k,v) return v end,{fred=10,bonzo=20}) is {10,20} _or_ {20,10}
555
-- @usage pairmap(function(k,v) return {k,v},k end,{one=1,two=2}) is {one={'one',1},two={'two',2}}
556
function tablex.pairmap(fun,t,...)
140✔
557
    assert_arg_iterable(1,t)
60✔
558
    fun = function_arg(1,fun)
60✔
559
    local res = {}
60✔
560
    for k,v in pairs(t) do
236✔
561
        local rv,rk = fun(k,v,...)
176✔
562
        if rk then
176✔
563
            if res[rk] then
60✔
564
                if type(res[rk]) == 'table' then
8✔
565
                    table.insert(res[rk],rv)
4✔
566
                else
567
                    res[rk] = {res[rk], rv}
4✔
568
                end
569
            else
570
                res[rk] = rv
52✔
571
            end
572
        else
573
            res[#res+1] = rv
116✔
574
        end
575
    end
576
    return res
60✔
577
end
578

579
local function keys_op(i,v) return i end
172✔
580

581
--- return all the keys of a table in arbitrary order.
582
-- @within Extraction
583
-- @tab t A list-like table where the values are the keys of the input table
584
function tablex.keys(t)
140✔
585
    assert_arg_iterable(1,t)
12✔
586
    return makelist(tablex.pairmap(keys_op,t))
12✔
587
end
588

589
local function values_op(i,v) return v end
152✔
590

591
--- return all the values of the table in arbitrary order
592
-- @within Extraction
593
-- @tab t A list-like table where the values are the values of the input table
594
function tablex.values(t)
140✔
595
    assert_arg_iterable(1,t)
4✔
596
    return makelist(tablex.pairmap(values_op,t))
4✔
597
end
598

599
local function index_map_op (i,v) return i,v end
172✔
600

601
--- create an index map from a list-like table. The original values become keys,
602
-- and the associated values are the indices into the original list.
603
-- @array t a list-like table
604
-- @return a map-like table
605
function tablex.index_map (t)
140✔
606
    assert_arg_indexable(1,t)
8✔
607
    return makemap(tablex.pairmap(index_map_op,t))
8✔
608
end
609

610
local function set_op(i,v) return true,v end
140✔
611

612
--- create a set from a list-like table. A set is a table where the original values
613
-- become keys, and the associated values are all true.
614
-- @array t a list-like table
615
-- @return a set (a map-like table)
616
function tablex.makeset (t)
140✔
617
    assert_arg_indexable(1,t)
×
618
    return setmetatable(tablex.pairmap(set_op,t),require('pl.Set'))
×
619
end
620

621
--- combine two tables, either as union or intersection. Corresponds to
622
-- set operations for sets () but more general. Not particularly
623
-- useful for list-like tables.
624
-- @within Merging
625
-- @tab t1 a table
626
-- @tab t2 a table
627
-- @bool dup true for a union, false for an intersection.
628
-- @usage merge({alice=23,fred=34},{bob=25,fred=34}) is {fred=34}
629
-- @usage merge({alice=23,fred=34},{bob=25,fred=34},true) is {bob=25,fred=34,alice=23}
630
-- @see tablex.index_map
631
function tablex.merge (t1,t2,dup)
140✔
632
    assert_arg_iterable(1,t1)
24✔
633
    assert_arg_iterable(2,t2)
24✔
634
    local res = {}
24✔
635
    for k,v in pairs(t1) do
80✔
636
        if dup or t2[k] then res[k] = v end
56✔
637
    end
638
    if dup then
24✔
639
      for k,v in pairs(t2) do
32✔
640
        res[k] = v
20✔
641
      end
642
    end
643
    return setmeta(res,t1,'Map')
24✔
644
end
645

646
--- the union of two map-like tables.
647
-- If there are duplicate keys, the second table wins.
648
-- @tab t1 a table
649
-- @tab t2 a table
650
-- @treturn tab
651
-- @see tablex.merge
652
function tablex.union(t1, t2)
140✔
653
    return tablex.merge(t1, t2, true)
×
654
end
655

656
--- the intersection of two map-like tables.
657
-- @tab t1 a table
658
-- @tab t2 a table
659
-- @treturn tab
660
-- @see tablex.merge
661
function tablex.intersection(t1, t2)
140✔
662
    return tablex.merge(t1, t2, false)
×
663
end
664

665
--- a new table which is the difference of two tables.
666
-- With sets (where the values are all true) this is set difference and
667
-- symmetric difference depending on the third parameter.
668
-- @within Merging
669
-- @tab s1 a map-like table or set
670
-- @tab s2 a map-like table or set
671
-- @bool symm symmetric difference (default false)
672
-- @return a map-like table or set
673
function tablex.difference (s1,s2,symm)
140✔
674
    assert_arg_iterable(1,s1)
24✔
675
    assert_arg_iterable(2,s2)
24✔
676
    local res = {}
24✔
677
    for k,v in pairs(s1) do
64✔
678
        if s2[k] == nil then res[k] = v end
40✔
679
    end
680
    if symm then
24✔
681
        for k,v in pairs(s2) do
20✔
682
            if s1[k] == nil then res[k] = v end
12✔
683
        end
684
    end
685
    return setmeta(res,s1,'Map')
24✔
686
end
687

688
--- A table where the key/values are the values and value counts of the table.
689
-- @array t a list-like table
690
-- @func cmp a function that defines equality (otherwise uses ==)
691
-- @return a map-like table
692
-- @see seq.count_map
693
function tablex.count_map (t,cmp)
140✔
694
    assert_arg_indexable(1,t)
4✔
695
    local res,mask = {},{}
4✔
696
    cmp = function_arg(2,cmp or '==')
4✔
697
    local n = #t
4✔
698
    for i = 1,#t do
20✔
699
        local v = t[i]
16✔
700
        if not mask[v] then
16✔
701
            mask[v] = true
12✔
702
            -- check this value against all other values
703
            res[v] = 1  -- there's at least one instance
12✔
704
            for j = i+1,n do
32✔
705
                local w = t[j]
20✔
706
                local ok = cmp(v,w)
20✔
707
                if ok then
20✔
708
                    res[v] = res[v] + 1
4✔
709
                    mask[w] = true
4✔
710
                end
711
            end
712
        end
713
    end
714
    return makemap(res)
4✔
715
end
716

717
--- filter an array's values using a predicate function
718
-- @within MappingAndFiltering
719
-- @array t a list-like table
720
-- @func pred a boolean function
721
-- @param arg optional argument to be passed as second argument of the predicate
722
function tablex.filter (t,pred,arg)
140✔
723
    assert_arg_indexable(1,t)
12✔
724
    pred = function_arg(2,pred)
12✔
725
    local res,k = {},1
12✔
726
    for i = 1,#t do
164✔
727
        local v = t[i]
152✔
728
        if pred(v,arg) then
152✔
729
            res[k] = v
144✔
730
            k = k + 1
144✔
731
        end
732
    end
733
    return setmeta(res,t,'List')
12✔
734
end
735

736
--- return a table where each element is a table of the ith values of an arbitrary
737
-- number of tables. It is equivalent to a matrix transpose.
738
-- @within Merging
739
-- @usage zip({10,20,30},{100,200,300}) is {{10,100},{20,200},{30,300}}
740
-- @array ... arrays to be zipped
741
function tablex.zip(...)
140✔
742
    return tablex.mapn(function(...) return {...} end,...)
16✔
743
end
744

745
local _copy
UNCOV
746
function _copy (dest,src,idest,isrc,nsrc,clean_tail)
105✔
747
    idest = idest or 1
40✔
748
    isrc = isrc or 1
40✔
749
    local iend
750
    if not nsrc then
40✔
751
        nsrc = #src
20✔
752
        iend = #src
20✔
753
    else
754
        iend = isrc + min(nsrc-1,#src-isrc)
20✔
755
    end
756
    if dest == src then -- special case
40✔
757
        if idest > isrc and iend >= idest then -- overlapping ranges
4✔
758
            src = tablex.sub(src,isrc,nsrc)
4✔
759
            isrc = 1; iend = #src
4✔
760
        end
761
    end
762
    for i = isrc,iend do
116✔
763
        dest[idest] = src[i]
76✔
764
        idest = idest + 1
76✔
765
    end
766
    if clean_tail then
40✔
767
        tablex.clear(dest,idest)
16✔
768
    end
769
    return dest
40✔
770
end
771

772
--- copy an array into another one, clearing `dest` after `idest+nsrc`, if necessary.
773
-- @within Copying
774
-- @array dest a list-like table
775
-- @array src a list-like table
776
-- @int[opt=1] idest where to start copying values into destination
777
-- @int[opt=1] isrc where to start copying values from source
778
-- @int[opt=#src] nsrc number of elements to copy from source
779
function tablex.icopy (dest,src,idest,isrc,nsrc)
140✔
780
    assert_arg_indexable(1,dest)
16✔
781
    assert_arg_indexable(2,src)
16✔
782
    return _copy(dest,src,idest,isrc,nsrc,true)
16✔
783
end
784

785
--- copy an array into another one.
786
-- @within Copying
787
-- @array dest a list-like table
788
-- @array src a list-like table
789
-- @int[opt=1] idest where to start copying values into destination
790
-- @int[opt=1] isrc where to start copying values from source
791
-- @int[opt=#src] nsrc number of elements to copy from source
792
function tablex.move (dest,src,idest,isrc,nsrc)
140✔
793
    assert_arg_indexable(1,dest)
24✔
794
    assert_arg_indexable(2,src)
24✔
795
    return _copy(dest,src,idest,isrc,nsrc,false)
24✔
796
end
797

798
function tablex._normalize_slice(self,first,last)
140✔
799
  local sz = #self
40✔
800
  if not first then first=1 end
40✔
801
  if first<0 then first=sz+first+1 end
40✔
802
  -- make the range _inclusive_!
803
  if not last then last=sz end
40✔
804
  if last < 0 then last=sz+1+last end
40✔
805
  return first,last
40✔
806
end
807

808
--- Extract a range from a table, like  'string.sub'.
809
-- If first or last are negative then they are relative to the end of the list
810
-- eg. sub(t,-2) gives last 2 entries in a list, and
811
-- sub(t,-4,-2) gives from -4th to -2nd
812
-- @within Extraction
813
-- @array t a list-like table
814
-- @int first An index
815
-- @int last An index
816
-- @return a new List
817
function tablex.sub(t,first,last)
140✔
818
    assert_arg_indexable(1,t)
32✔
819
    first,last = tablex._normalize_slice(t,first,last)
32✔
820
    local res={}
32✔
821
    for i=first,last do append(res,t[i]) end
120✔
822
    return setmeta(res,t,'List')
32✔
823
end
824

825
--- set an array range to a value. If it's a function we use the result
826
-- of applying it to the indices.
827
-- @array t a list-like table
828
-- @param val a value
829
-- @int[opt=1] i1 start range
830
-- @int[opt=#t] i2 end range
831
function tablex.set (t,val,i1,i2)
140✔
832
    assert_arg_indexable(1,t)
24✔
833
    i1,i2 = i1 or 1,i2 or #t
24✔
834
    if types.is_callable(val) then
24✔
835
        for i = i1,i2 do
32✔
836
            t[i] = val(i)
24✔
837
        end
838
    else
839
        for i = i1,i2 do
52✔
840
            t[i] = val
36✔
841
        end
842
    end
843
end
844

845
--- create a new array of specified size with initial value.
846
-- @int n size
847
-- @param val initial value (can be `nil`, but don't expect `#` to work!)
848
-- @return the table
849
function tablex.new (n,val)
140✔
850
    local res = {}
4✔
851
    tablex.set(res,val,1,n)
4✔
852
    return res
4✔
853
end
854

855
--- clear out the contents of a table.
856
-- @array t a list
857
-- @param istart optional start position
858
function tablex.clear(t,istart)
140✔
859
    istart = istart or 1
16✔
860
    for i = istart,#t do remove(t) end
72✔
861
end
862

863
--- insert values into a table.
864
-- similar to `table.insert` but inserts values from given table `values`,
865
-- not the object itself, into table `t` at position `pos`.
866
-- @within Copying
867
-- @array t the list
868
-- @int[opt] position (default is at end)
869
-- @array values
870
function tablex.insertvalues(t, ...)
140✔
871
    assert_arg(1,t,'table')
16✔
872
    local pos, values
873
    if select('#', ...) == 1 then
16✔
874
        pos,values = #t+1, ...
8✔
875
    else
876
        pos,values = ...
8✔
877
    end
878
    if #values > 0 then
16✔
879
        for i=#t,pos,-1 do
60✔
880
            t[i+#values] = t[i]
44✔
881
        end
882
        local offset = 1 - pos
16✔
883
        for i=pos,pos+#values-1 do
108✔
884
            t[i] = values[i + offset]
92✔
885
        end
886
    end
887
    return t
16✔
888
end
889

890
--- remove a range of values from a table.
891
-- End of range may be negative.
892
-- @array t a list-like table
893
-- @int i1 start index
894
-- @int i2 end index
895
-- @return the table
896
function tablex.removevalues (t,i1,i2)
140✔
897
    assert_arg(1,t,'table')
4✔
898
    i1,i2 = tablex._normalize_slice(t,i1,i2)
4✔
899
    for i = i1,i2 do
12✔
900
        remove(t,i1)
8✔
901
    end
902
    return t
4✔
903
end
904

905
local _find
906
_find = function (t,value,tables)
907
    for k,v in pairs(t) do
110✔
908
        if v == value then return k end
68✔
909
    end
910
    for k,v in pairs(t) do
80✔
911
        if not tables[v] and type(v) == 'table' then
54✔
912
            tables[v] = true
38✔
913
            local res = _find(v,value,tables)
38✔
914
            if res then
38✔
915
                res = tostring(res)
16✔
916
                if type(k) ~= 'string' then
16✔
917
                    return '['..k..']'..res
×
918
                else
919
                    return k..'.'..res
16✔
920
                end
921
            end
922
        end
923
    end
924
end
925

926
--- find a value in a table by recursive search.
927
-- @within Finding
928
-- @tab t the table
929
-- @param value the value
930
-- @array[opt] exclude any tables to avoid searching
931
-- @return a fieldspec, e.g. 'a.b' or 'math.sin'
932
-- @usage search(_G,math.sin,{package.path}) == 'math.sin'
933
function tablex.search (t,value,exclude)
140✔
934
    assert_arg_iterable(1,t)
12✔
935
    local tables = {[t]=true}
12✔
936
    if exclude then
12✔
937
        for _,v in pairs(exclude) do tables[v] = true end
8✔
938
    end
939
    return _find(t,value,tables)
12✔
940
end
941

942
--- return an iterator to a table sorted by its keys
943
-- @within Iterating
944
-- @tab t the table
945
-- @func f an optional comparison function (f(x,y) is true if x < y)
946
-- @usage for k,v in tablex.sort(t) do print(k,v) end
947
-- @return an iterator to traverse elements sorted by the keys
948
function tablex.sort(t,f)
140✔
949
    local keys = {}
4✔
950
    for k in pairs(t) do keys[#keys + 1] = k end
44✔
951
    tsort(keys,f)
4✔
952
    local i = 0
4✔
953
    return function()
954
        i = i + 1
44✔
955
        return keys[i], t[keys[i]]
44✔
956
    end
957
end
958

959
--- return an iterator to a table sorted by its values
960
-- @within Iterating
961
-- @tab t the table
962
-- @func f an optional comparison function (f(x,y) is true if x < y)
963
-- @usage for k,v in tablex.sortv(t) do print(k,v) end
964
-- @return an iterator to traverse elements sorted by the values
965
function tablex.sortv(t,f)
140✔
966
    f = function_arg(2, f or '<')
4✔
967
    local keys = {}
4✔
968
    for k in pairs(t) do keys[#keys + 1] = k end
44✔
969
    tsort(keys,function(x, y) return f(t[x], t[y]) end)
111✔
970
    local i = 0
4✔
971
    return function()
972
        i = i + 1
44✔
973
        return keys[i], t[keys[i]]
44✔
974
    end
975
end
976

977
--- modifies a table to be read only.
978
-- This only offers weak protection. Tables can still be modified with
979
-- `table.insert` and `rawset`.
980
--
981
-- *NOTE*: for Lua 5.1 length, pairs and ipairs will not work, since the
982
-- equivalent metamethods are only available in Lua 5.2 and newer.
983
-- @tab t the table
984
-- @return the table read only (a proxy).
985
function tablex.readonly(t)
140✔
986
    local mt = {
4✔
987
        __index=t,
4✔
988
        __newindex=function(t, k, v) error("Attempt to modify read-only table", 2) end,
8✔
989
        __pairs=function() return pairs(t) end,
7✔
990
        __ipairs=function() return ipairs(t) end,
6✔
991
        __len=function() return #t end,
7✔
992
        __metatable=false
4✔
993
    }
994
    return setmetatable({}, mt)
4✔
995
end
996

997
return tablex
140✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc