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

lunarmodules / Penlight / 477

12 Feb 2024 09:01AM UTC coverage: 89.675% (+0.7%) from 88.938%
477

push

appveyor

web-flow
fix(doc): typo in example (#463)

5489 of 6121 relevant lines covered (89.67%)

346.11 hits per line

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

97.8
/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')
280✔
8
local types = require ('pl.types')
280✔
9
local getmetatable,setmetatable,require = getmetatable,setmetatable,require
280✔
10
local tsort,append,remove = table.sort,table.insert,table.remove
280✔
11
local min = math.min
280✔
12
local pairs,type,unpack,select,tostring = pairs,type,utils.unpack,select,tostring
280✔
13
local function_arg = utils.function_arg
280✔
14
local assert_arg = utils.assert_arg
280✔
15

16
local tablex = {}
280✔
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)
2,000✔
23
    return mt and setmetatable(res, mt) or res
2,000✔
24
end
25

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

30
local function makemap(m)
31
    return setmetatable(m, require('pl.Map'))
24✔
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
2,100✔
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
3,336✔
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
24✔
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)
280✔
62
    assert_arg_writeable(1,t1)
16✔
63
    assert_arg_iterable(2,t2)
16✔
64
    for k,v in pairs(t2) do
72✔
65
        t1[k] = v
56✔
66
    end
67
    return t1
16✔
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)
280✔
79
    assert_arg_iterable(1,t)
24✔
80
    local i = 0
24✔
81
    for k in pairs(t) do i = i + 1 end
56✔
82
    return i
24✔
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)
280✔
90
    assert_arg_iterable(1,t)
208✔
91
    local res = {}
208✔
92
    for k,v in pairs(t) do
10,872✔
93
        res[k] = v
10,664✔
94
    end
95
    return res
208✔
96
end
97

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

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

124
local abs = math.abs
280✔
125

126
local function cycle_aware_compare(t1,t2,ignore_mt,eps,cache)
127
    if cache[t1] and cache[t1][t2] then return true end
11,510✔
128
    local ty1 = type(t1)
11,502✔
129
    local ty2 = type(t2)
11,502✔
130
    if ty1 ~= ty2 then return false end
11,502✔
131
    -- non-table types can be directly compared
132
    if ty1 ~= 'table' then
11,502✔
133
        if ty1 == 'number' and eps then return abs(t1-t2) < eps end
7,894✔
134
        return t1 == t2
7,612✔
135
    end
136
    -- as well as tables which have the metamethod __eq
137
    local mt = getmetatable(t1)
3,608✔
138
    if not ignore_mt and mt and mt.__eq then return t1 == t2 end
3,608✔
139
    for k1 in pairs(t1) do
12,958✔
140
        if t2[k1]==nil then return false end
9,308✔
141
    end
142
    for k2 in pairs(t2) do
12,958✔
143
        if t1[k2]==nil then return false end
9,308✔
144
    end
145
    cache[t1] = cache[t1] or {}
3,608✔
146
    cache[t1][t2] = true
3,608✔
147
    for k1,v1 in pairs(t1) do
12,958✔
148
        local v2 = t2[k1]
9,308✔
149
        if not cycle_aware_compare(v1,v2,ignore_mt,eps,cache) then return false end
14,158✔
150
    end
151
    return true
3,608✔
152
end
153

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

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

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

219

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

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

259

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

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

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

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

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

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

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

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

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

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

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

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

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

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

581
local function keys_op(i,v) return i end
344✔
582

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

591
local function values_op(i,v) return v end
304✔
592

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

601
local function index_map_op (i,v) return i,v end
344✔
602

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

612
local function set_op(i,v) return true,v end
280✔
613

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

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

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

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

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

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

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

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

747
local _copy
748
function _copy (dest,src,idest,isrc,nsrc,clean_tail)
245✔
749
    idest = idest or 1
80✔
750
    isrc = isrc or 1
80✔
751
    local iend
752
    if not nsrc then
80✔
753
        nsrc = #src
40✔
754
        iend = #src
40✔
755
    else
756
        iend = isrc + min(nsrc-1,#src-isrc)
40✔
757
    end
758
    if dest == src then -- special case
80✔
759
        if idest > isrc and iend >= idest then -- overlapping ranges
8✔
760
            src = tablex.sub(src,isrc,nsrc)
12✔
761
            isrc = 1; iend = #src
8✔
762
        end
763
    end
764
    for i = isrc,iend do
232✔
765
        dest[idest] = src[i]
152✔
766
        idest = idest + 1
152✔
767
    end
768
    if clean_tail then
80✔
769
        tablex.clear(dest,idest)
32✔
770
    end
771
    return dest
80✔
772
end
773

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

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

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

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

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

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

857
--- clear out the contents of a table.
858
-- @array t a list
859
-- @param istart optional start position
860
function tablex.clear(t,istart)
280✔
861
    istart = istart or 1
32✔
862
    for i = istart,#t do remove(t) end
162✔
863
end
864

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

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

907
local _find
908
_find = function (t,value,tables)
909
    for k,v in pairs(t) do
216✔
910
        if v == value then return k end
134✔
911
    end
912
    for k,v in pairs(t) do
156✔
913
        if not tables[v] and type(v) == 'table' then
106✔
914
            tables[v] = true
74✔
915
            local res = _find(v,value,tables)
74✔
916
            if res then
74✔
917
                res = tostring(res)
32✔
918
                if type(k) ~= 'string' then
32✔
919
                    return '['..k..']'..res
×
920
                else
921
                    return k..'.'..res
32✔
922
                end
923
            end
924
        end
925
    end
926
end
927

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

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

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

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

999
return tablex
280✔
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