# General Lua Libraries for Lua 5.1, 5.2 & 5.3 # Copyright (C) 2011-2018 stdlib authors before: | base_module = 'table' this_module = 'std.table' global_table = '_G' extend_base = {'clone', 'clone_select', 'depair', 'empty', 'enpair', 'insert', 'invert', 'keys', 'maxn', 'merge', 'merge_select', 'new', 'pack', 'project', 'remove', 'size', 'sort', 'unpack', 'values'} M = require(this_module) specify std.table: - context when required: - context by name: - it does not touch the global table: expect(show_apis {added_to=global_table, by=this_module}). to_equal {} - it does not touch the core table table: expect(show_apis {added_to=base_module, by=this_module}). to_equal {} - it contains apis from the core table table: expect(show_apis {from=base_module, not_in=this_module}). to_contain.a_permutation_of(extend_base) - context via the std module: - it does not touch the global table: expect(show_apis {added_to=global_table, by='std'}).to_equal {} - it does not touch the core table table: expect(show_apis {added_to=base_module, by='std'}).to_equal {} - describe clone: - before: subject = {k1={'v1'}, k2={'v2'}, k3={'v3'}} withmt = setmetatable(M.clone(subject), {'meta!'}) f = M.clone - context with bad arguments: badargs.diagnose(f, 'std.table.clone(table, [table], ?boolean|:nometa)') - it does not just return the subject: expect(f(subject)).not_to_be(subject) - it does copy the subject: expect(f(subject)).to_equal(subject) - it only makes a shallow copy of field values: expect(f(subject).k1).to_be(subject.k1) - it does not perturb the original subject: target = {k1=subject.k1, k2=subject.k2, k3=subject.k3} copy = f(subject) expect(subject).to_equal(target) expect(subject).to_be(subject) - context with metatables: - it copies the metatable by default: expect(getmetatable(f(withmt))).to_be(getmetatable(withmt)) - it treats non-table arg2 as nometa parameter: expect(getmetatable(f(withmt, ':nometa'))).to_be(nil) - it treats table arg2 as a map parameter: expect(getmetatable(f(withmt, {}))).to_be(getmetatable(withmt)) - it supports 3 arguments with nometa as arg3: expect(getmetatable(f(withmt, {}, ':nometa'))).to_be(nil) - context when renaming some keys: - it renames during cloning: target = {newkey=subject.k1, k2=subject.k2, k3=subject.k3} expect(f(subject, {k1 = 'newkey'})).to_equal(target) - it does not perturb the value in the renamed key field: expect(f(subject, {k1 = 'newkey'}).newkey).to_be(subject.k1) - describe clone_select: - before: subject = {k1={'v1'}, k2={'v2'}, k3={'v3'}} withmt = setmetatable(M.clone(subject), {'meta!'}) f = M.clone_select - context with bad arguments: badargs.diagnose(f, 'std.table.clone_select(table, [table], ?boolean|:nometa)') - it does not just return the subject: expect(f(subject)).not_to_be(subject) - it copies the keys selected: expect(f(subject, {'k1', 'k2'})).to_equal({k1={'v1'}, k2={'v2'}}) - it does copy the subject when supplied with a full list of keys: expect(f(subject, {'k1', 'k2', 'k3'})).to_equal(subject) - it only makes a shallow copy: expect(f(subject, {'k1'}).k1).to_be(subject.k1) - it does not perturb the original subject: target = {k1=subject.k1, k2=subject.k2, k3=subject.k3} copy = f(subject, {'k1', 'k2', 'k3'}) expect(subject).to_equal(target) expect(subject).to_be(subject) - context with metatables: - it treats non-table arg2 as nometa parameter: expect(getmetatable(f(withmt, ':nometa'))).to_be(nil) - it treats table arg2 as a map parameter: expect(getmetatable(f(withmt, {}))).to_be(getmetatable(withmt)) expect(getmetatable(f(withmt, {'k1'}))).to_be(getmetatable(withmt)) - it supports 3 arguments with nometa as arg3: expect(getmetatable(f(withmt, {}, ':nometa'))).to_be(nil) expect(getmetatable(f(withmt, {'k1'}, ':nometa'))).to_be(nil) - describe depair: - before: t = {'first', 'second', third = 4} l = M.enpair(t) f = M.depair - context with bad arguments: badargs.diagnose(f, 'std.table.depair(list of lists)') - it returns a primitive table: expect(objtype(f(l))).to_be 'table' - it works with an empty table: expect(f {}).to_equal {} - it is the inverse of enpair: expect(f(l)).to_equal(t) - describe empty: - before: f = M.empty - context with bad arguments: badargs.diagnose(f, 'std.table.empty(table)') - it returns true for an empty table: expect(f {}).to_be(true) expect(f {nil}).to_be(true) - it returns false for a non-empty table: expect(f {'stuff'}).to_be(false) expect(f {{}}).to_be(false) expect(f {false}).to_be(false) - describe enpair: - before: t = {'first', 'second', third = 4} l = M.enpair(t) f = M.enpair - context with bad arguments: badargs.diagnose(f, 'std.table.enpair(table)') - it returns a table: expect(objtype(f(t))).to_be 'table' - it works for an empty table: expect(f {}).to_equal {} - it turns a table into a table of pairs: expect(f(t)).to_equal {{1, 'first'}, {2, 'second'}, {'third', 4}} - it is the inverse of depair: expect(f(t)).to_equal(l) - describe insert: - before: f, badarg = init(M, this_module, 'insert') - context with bad arguments: badargs.diagnose(f, 'std.table.insert(table, [int], any)') examples { ['it diagnoses more than 2 arguments with no pos'] = function() pending '#issue 76' expect(f({}, false, false)).to_raise(badarg(3)) end } examples { ['it diagnoses out of bounds pos arguments'] = function() expect(f({}, 0, 'x')).to_raise 'position 0 out of bounds' expect(f({}, 2, 'x')).to_raise 'position 2 out of bounds' expect(f({1}, 5, 'x')).to_raise 'position 5 out of bounds' end } - it returns the modified table: t = {} expect(f(t, 1)).to_be(t) - it append a new element at the end by default: expect(f({1, 2}, 'x')).to_equal {1, 2, 'x'} - it fills holes by default: expect(f({1, 2, [5]=3}, 'x')).to_equal {1, 2, 'x', [5]=3} - it respects __len when appending: t = setmetatable({1, 2, [5]=3}, {__len = function() return 42 end}) expect(f(t, 'x')).to_equal {1, 2, [5]=3, [43]='x'} - it moves other elements up if necessary: expect(f({1, 2}, 1, 'x')).to_equal {'x', 1, 2} expect(f({1, 2}, 2, 'x')).to_equal {1, 'x', 2} expect(f({1, 2}, 3, 'x')).to_equal {1, 2, 'x'} - it inserts a new element according to pos argument: expect(f({}, 1, 'x')).to_equal {'x'} - describe invert: - before: subject = {k1=1, k2=2, k3=3} f = M.invert - context with bad arguments: badargs.diagnose(f, 'std.table.invert(table)') - it returns a new table: expect(f(subject)).not_to_be(subject) - it inverts keys and values in the returned table: expect(f(subject)).to_equal {'k1', 'k2', 'k3'} - it is reversible: expect(f(f(subject))).to_equal(subject) - it seems to copy a list of 1..n numbers: subject = {1, 2, 3} expect(f(subject)).to_copy(subject) - describe keys: - before: subject = {k1=1, k2=2, k3=3} f = M.keys - context with bad arguments: badargs.diagnose(f, 'std.table.keys(table)') - it returns an empty list when subject is empty: expect(f {}).to_equal {} - it makes a list of table keys: cmp = function(a, b) return a < b end expect(M.sort(f(subject), cmp)).to_equal {'k1', 'k2', 'k3'} - it does not guarantee stable ordering: subject = {} -- is this a good test? there is a vanishingly small possibility the -- returned table will have all 10000 keys in the same order... for i = 10000, 1, -1 do table.insert(subject, i) end expect(f(subject)).not_to_equal(subject) - describe maxn: - before: f = M.maxn - context with bad arguments: badargs.diagnose(f, 'std.table.maxn(table)') - it returns the largest numeric key of a table: expect(f {'a', 'b', 'c'}).to_be(3) expect(f {1, 2, 5, a=10, 3}).to_be(4) - it works with an empty table: expect(f {}).to_be(0) - it ignores holes: expect(f {1, 2, [5]=3}).to_be(5) - it ignores __len metamethod: t = setmetatable({1, 2, [5]=3}, {__len = function() return 42 end}) expect(f(t)).to_be(5) - describe merge: - before: | -- Additional merge keys which are moderately unusual t1 = {k1={'v1'}, k2='if', k3={'?'}} t2 = {['if']=true, [{'?'}]=false, _='underscore', k3=t1.k1} t1mt = setmetatable(M.clone(t1), {'meta!'}) target = {} for k, v in pairs(t1) do target[k] = v end for k, v in pairs(t2) do target[k] = v end f, badarg = init(M, this_module, 'merge') - context with bad arguments: badargs.diagnose(f, 'std.table.merge(table, table, [table], ?boolean|:nometa)') examples { ['it diagnoses more than 2 arguments with no pos'] = function() pending '#issue 76' expect(f({}, {}, ':nometa', false)).to_raise(badarg(4)) end } - it does not create a whole new table: expect(f(t1, t2)).to_be(t1) - it does not change t1 when t2 is empty: expect(f(t1, {})).to_be(t1) - it copies t2 when t1 is empty: expect(f({}, t1)).to_copy(t1) - it merges keys from t2 into t1: expect(f(t1, t2)).to_equal(target) - it gives precedence to values from t2: original = M.clone(t1) m = f(t1, t2) -- Merge is destructive, do it once only. expect(m.k3).to_be(t2.k3) expect(m.k3).not_to_be(original.k3) - it only makes a shallow copy of field values: expect(f({}, t1).k1).to_be(t1.k1) - context with metatables: - it copies the metatable by default: expect(getmetatable(f({}, t1mt))).to_be(getmetatable(t1mt)) expect(getmetatable(f({}, t1mt, {'k1'}))).to_be(getmetatable(t1mt)) - it treats non-table arg3 as nometa parameter: expect(getmetatable(f({}, t1mt, ':nometa'))).to_be(nil) - it treats table arg3 as a map parameter: expect(getmetatable(f({}, t1mt, {}))).to_be(getmetatable(t1mt)) expect(getmetatable(f({}, t1mt, {'k1'}))).to_be(getmetatable(t1mt)) - it supports 4 arguments with nometa as arg4: expect(getmetatable(f({}, t1mt, {}, ':nometa'))).to_be(nil) expect(getmetatable(f({}, t1mt, {'k1'}, ':nometa'))).to_be(nil) - context when renaming some keys: - it renames during merging: target = {newkey=t1.k1, k2=t1.k2, k3=t1.k3} expect(f({}, t1, {k1 = 'newkey'})).to_equal(target) - it does not perturb the value in the renamed key field: expect(f({}, t1, {k1 = 'newkey'}).newkey).to_be(t1.k1) - describe merge_select: - before: | -- Additional merge keys which are moderately unusual tablekey = {'?'} t1 = {k1={'v1'}, k2='if', k3={'?'}} t1mt = setmetatable(M.clone(t1), {'meta!'}) t2 = {['if']=true, [tablekey]=false, _='underscore', k3=t1.k1} t2keys = {'if', tablekey, '_', 'k3'} target = {} for k, v in pairs(t1) do target[k] = v end for k, v in pairs(t2) do target[k] = v end f, badarg = init(M, this_module, 'merge_select') - context with bad arguments: badargs.diagnose(f, 'std.table.merge_select(table, table, [table], ?boolean|:nometa)') examples { ['it diagnoses more than 2 arguments with no pos'] = function() pending '#issue 76' expect(f({}, {}, ':nometa', false)).to_raise(badarg(4)) end } - it does not create a whole new table: expect(f(t1, t2)).to_be(t1) - it does not change t1 when t2 is empty: expect(f(t1, {})).to_be(t1) - it does not change t1 when key list is empty: expect(f(t1, t2, {})).to_be(t1) - it copies the named fields: expect(f({}, t2, t2keys)).to_equal(t2) - it makes a shallow copy: expect(f({}, t1, {'k1'}).k1).to_be(t1.k1) - it copies exactly named fields of t2 when t1 is empty: expect(f({}, t1, {'k1', 'k2', 'k3'})).to_copy(t1) - it merges keys from t2 into t1: expect(f(t1, t2, t2keys)).to_equal(target) - it gives precedence to values from t2: original = M.clone(t1) m = f(t1, t2, t2keys) -- Merge is destructive, do it once only. expect(m.k3).to_be(t2.k3) expect(m.k3).not_to_be(original.k3) - context with metatables: - it copies the metatable by default: expect(getmetatable(f({}, t1mt))).to_be(getmetatable(t1mt)) expect(getmetatable(f({}, t1mt, {'k1'}))).to_be(getmetatable(t1mt)) - it treats non-table arg3 as nometa parameter: expect(getmetatable(f({}, t1mt, ':nometa'))).to_be(nil) - it treats table arg3 as a map parameter: expect(getmetatable(f({}, t1mt, {}))).to_be(getmetatable(t1mt)) expect(getmetatable(f({}, t1mt, {'k1'}))).to_be(getmetatable(t1mt)) - it supports 4 arguments with nometa as arg4: expect(getmetatable(f({}, t1mt, {}, ':nometa'))).to_be(nil) expect(getmetatable(f({}, t1mt, {'k1'}, ':nometa'))).to_be(nil) - describe new: - before: f = M.new - context with bad arguments: badargs.diagnose(f, 'std.table.new(?any, ?table)') - context when not setting a default: - before: default = nil - it returns a new table when nil is passed: expect(f(default, nil)).to_equal {} - it returns any table passed in: t = {'unique table'} expect(f(default, t)).to_be(t) - context when setting a default: - before: default = 'default' - it returns a new table when nil is passed: expect(f(default, nil)).to_equal {} - it returns any table passed in: t = {'unique table'} expect(f(default, t)).to_be(t) - it returns the stored value for existing keys: t = f('default') v = {'unique value'} t[1] = v expect(t[1]).to_be(v) - it returns the constructor default for unset keys: t = f('default') expect(t[1]).to_be 'default' - it returns the actual default object: default = {'unique object'} t = f(default) expect(t[1]).to_be(default) - describe pack: - before: unpack = unpack or table.unpack t = {'one', 'two', 'five', n=3} f = M.pack - it creates an empty table with no arguments: expect(f()).to_equal {n=0} - it creates a table with arguments as elements: expect(f('one', 'two', 'five')).to_equal(t) - it is the inverse operation to unpack: expect(f(unpack(t))).to_equal(t) - it saves the tuple length in field n: expect(f(1, 2, 5).n).to_be(3) expect(f('', false, nil).n).to_be(3) expect(f(nil, nil, nil).n).to_be(3) - describe project: - before: l = { {first = false, second = true, third = true}, {first = 1, second = 2, third = 3}, {first = '1st', second = '2nd', third = '3rd'}, } f = M.project - context with bad arguments: badargs.diagnose(f, 'std.table.project(any, list of tables)') - it returns a table: expect(objtype(f('third', l))).to_be 'table' - it works with an empty table: expect(f('third', {})).to_equal {} - it projects a table of fields from a table of tables: expect(f('third', l)).to_equal {true, 3, '3rd'} - it projects fields with a falsey value correctly: expect(f('first', l)).to_equal {false, 1, '1st'} - describe remove: - before: f = M.remove - context with bad arguments: badargs.diagnose(f, 'std.table.remove(table, ?int)') examples { ['it diagnoses out of bounds pos arguments'] = function() expect(f({1}, 0)).to_raise 'position 0 out of bounds' expect(f({1}, 3)).to_raise 'position 3 out of bounds' expect(f({1}, 5)).to_raise 'position 5 out of bounds' end } - it returns the removed element: t = {'one', 'two', 'five'} expect(f({'one', 2, 5}, 1)).to_be 'one' - it removes an element from the end by default: expect(f {1, 2, 'five'}).to_be 'five' - it ignores holes: t = {'second', 'first', [5]='invisible'} expect(f(t)).to_be 'first' expect(f(t)).to_be 'second' - it respects __len when defaulting pos: t = setmetatable({1, 2, [43]='invisible'}, {__len = function() return 42 end}) expect(f(t)).to_be(nil) expect(f(t)).to_be(nil) expect(t).to_equal {1, 2, [43]='invisible'} - it moves other elements down if necessary: t = {1, 2, 5, 'third', 'first', 'second', 42} expect(f(t, 5)).to_be 'first' expect(t).to_equal {1, 2, 5, 'third', 'second', 42} expect(f(t, 5)).to_be 'second' expect(t).to_equal {1, 2, 5, 'third', 42} expect(f(t, 4)).to_be 'third' expect(t).to_equal {1, 2, 5, 42} - describe size: - before: | -- - 1 - ------- 2 ------- -- 3 -- subject = {'one', {{'two'}, 'three'}, four=5} f = M.size - context with bad arguments: badargs.diagnose(f, 'std.table.size(table)') - it counts the number of keys in a table: expect(f(subject)).to_be(3) - it counts no keys in an empty table: expect(f {}).to_be(0) - describe sort: - before: subject = {5, 2, 4, 1, 0, 3} target = {0, 1, 2, 3, 4, 5} cmp = function(a, b) return a < b end f = M.sort - context with bad arguments: badargs.diagnose(f, 'std.table.sort(table, ?function)') - it sorts elements in place: f(subject, cmp) expect(subject).to_equal(target) - it returns the sorted table: expect(f(subject, cmp)).to_equal(target) - describe unpack: - before: t = {'one', 'two', 'five'} f = M.unpack - it returns nil for an empty table: expect(f {}).to_be(nil) - it returns numeric indexed table elements: expect({f(t)}).to_equal(t) - it respects __len metamethod: function two(t) return setmetatable(t, {__len=function() return 2 end}) end expect(pack(f(two {})).n).to_be(2) expect(pack(f(two(t))).n).to_be(2) - it returns holes in numeric indices as nil: expect({f {nil, 2}}).to_equal {[2] = 2} expect({f {nil, nil, 3}}).to_equal {[3] = 3} expect({f {1, nil, nil, 4}}).to_equal {1, [4] = 4} - it is the inverse operation to pack: expect({f(M.pack('one', 'two', 'five'))}).to_equal(t) - describe values: - before: subject = {k1={1}, k2={2}, k3={3}} f = M.values - context with bad arguments: badargs.diagnose(f, 'std.table.values(table)') - it returns an empty list when subject is empty: expect(f {}).to_equal {} - it makes a list of table values: cmp = function(a, b) return a[1] < b[1] end expect(M.sort(f(subject), cmp)).to_equal {{1}, {2}, {3}} - it does guarantee stable ordering: subject = {} -- is this a good test? or just requiring an implementation quirk? for i = 10000, 1, -1 do table.insert(subject, i) end expect(f(subject)).to_equal(subject)