Module:UtilsTable

local p = {} local h = {}

local inspect = require("Module:UtilsTable/Inspect")

-- HELPERS

function h.append(tbl, val) tbl[table.maxn(tbl)+1] = val end

function h.mapper(iteratee) if type(iteratee) == "function" then return iteratee end if type(iteratee) == "string" or type(iteratee) == "number" then return p._property(iteratee) end end

function h.predicate(iteratee) if type(iteratee) == "function" then return iteratee end if type(iteratee) == "string" or type(iteratee) == "number" then return p._property(iteratee) end if type(iteratee) == "table" then return p._isMatch(iteratee) end return iteratee end

-- GENERAL

function p.hasArray(tbl) if type(tbl) ~= "table" then return false end return p.size(tbl) == 0 or table.maxn(tbl) > 0 end

function p.invert(tbl) local inverted = {} for k, v in pairs(tbl) do		inverted[v] = k	end return inverted end

function p.isArray(tbl) if type(tbl) ~= "table" then return false end for k, v in pairs(tbl) do		if type(k) ~= "number" then return false end end return true end

function p.isEmpty(tbl) return p.size(tbl) == 0 end

function p.isEqual(tbl, other) return p._isEqual(tbl)(other) end function p._isEqual(tbl) return function(other) if type(tbl) ~= "table" or type(other) ~= "table" then return tbl == other end return p.isMatch(other, tbl) and p.isMatch(tbl, other) end end

function p.isMatch(tbl, source) return p._isMatch(source)(tbl) end function p._isMatch(sourceTbl) return function(tbl) if sourceTbl == tbl then return true end if type(sourceTbl) ~= type(tbl) then return false end if p.size(sourceTbl) == 0 and p.size(tbl) > 0 then return false end for k, v in pairs(sourceTbl) do			if type(v) ~= "table" and v ~= tbl[k] then return false end if type(v) == "table" and not p.isMatch(tbl[k], v) then return false end end return true end end

function p.ivalues(tbl) local valueset = {} for i=1, table.maxn(tbl) do  	  if tbl[i] then table.insert(valueset, tbl[i]) end end return valueset end

function p.keyOf(tbl, val) for k, v in pairs(tbl) do		if v == val then return k		end end return nil end

function p.keys(tbl) local keyset = {} for k in pairs(tbl) do     keyset[#keyset + 1] = k   end return keyset end

function p.mapValues(tbl, iteratee) return p._mapValues(iteratee)(tbl) end function p._mapValues(iteratee) local mappingFunction = h.mapper(iteratee) return function(tbl) local result = {} for k, v in pairs(tbl) do			result[k] = mappingFunction(v) end return result end end

function p.merge(tbl, ...) if tbl == nil then return nil end for _, source in ipairs({...}) do		for k, v in pairs(source) do			if type(v) == "table" and type(tbl[k]) == "table" then tbl[k] = p.merge({}, tbl[k], v)			else tbl[k] = v			end end end return tbl end

local MAX_SINGLELINE = 50 function p.print(tbl, singleLine) return inspect(tbl, {		multiline = function(t)			if singleLine then				return false			end			local size = 0			for key, val in pairs(t) do				if type(val) == "table" then					return true				end				size = size + 1			end			local singleLineLength = #inspect(t, { multiline = false })			return singleLineLength > MAX_SINGLELINE or size > #t and size - #t > 1		end	}) end

function p.printPath(pathComponents) local path = "" for _, pathComponent in pairs(pathComponents or {}) do		if string.match(pathComponent, "^[A-Za-z_][A-Za-z_0-9]*$") then path = path .. "." .. pathComponent elseif string.match(pathComponent, "^%[.*%]$") then path = path .. pathComponent else path = path .. "[" .. p.print(pathComponent) .. "]"		end end return path end

function p.property(tbl, path) return p._property(path)(tbl) end function p._property(path) return function(tbl) if type(path) ~= "table" then path = { path } end local result = tbl for i, key in ipairs(path) do			result = result[key] or result[mw.text.trim(key, '%[%]"')]			if result == nil then				return result			end		end		return result	end end

function p.size(tbl) return #p.keys(tbl) end

function p.stringKeys(tbl) local result = {} for k in pairs(tbl) do		if type(k) == "string" then table.insert(result, k)		end end return result end

--	"Array" functions (using the `ipairs` iterator, mostly) --

function p.compact(array) local result = {} local j = 1 for i = 1, table.maxn(array) do		if array[i] then result[j] = array[i] j = j + 1 end end return result end

function p.concat(array, ...) local result = mw.clone(array) for i, arrayOrValue in ipairs({...}) do		if not p.isArray(arrayOrValue) then h.append(result, arrayOrValue) else for i = 1, table.maxn(arrayOrValue) do				h.append(result, arrayOrValue[i]) end end end return result end

function p.difference(array, other) local result = {} for i, v in ipairs(array) do		if not p.keyOf(other, v) then table.insert(result, v)		end end return result end

function p.filter(array, iteratee) return p._filter(iteratee)(array) end function p._filter(iteratee) return function(array) local predicateFn = h.predicate(iteratee) local results = {} for i = 1, table.maxn(array) do			if predicateFn(array[i]) then table.insert(results, array[i]) end end return results end end

function p.find(array, iteratee) return p._find(iteratee)(array) end function p._find(iteratee) local predicate = h.predicate(iteratee) return function(array) for i, v in ipairs(array) do			if predicate(v) then return v, i			end end return nil, nil end end

function p.findIndex(array, iteratee) local index = select(2, p.find(array, iteratee)) return index end

function p.findLast(array, iteratee) return p._findLast(iteratee)(array) end function p._findLast(iteratee) local predicate = h.predicate(iteratee) return function(array) local v, i = p.find(p.reverse(array), predicate) if not i then return nil, nil end return v, #array - i + 1 end end

function p.flatten(array) local result = {} for i = 1, table.maxn(array) do		result = p.concat(result, array[i]) end return result end

function p.flatMap(array, mappingFunction) return p._flatMap(mappingFunction)(array) end function p._flatMap(mappingFunction) return function(array) return p.flatten(p.map(array, mappingFunction)) end end

function p.groupBy(array, iteratee) return p._groupBy(iteratee)(array) end function p._groupBy(iteratee) local mappingFn = h.mapper(iteratee) return function(array) local result = {} for _, v in ipairs(array) do			local groupingKey = mappingFn(v) if groupingKey then local group = result[groupingKey] or {} result[groupingKey] = p.concat(group, v)			end end return result end end

function p.includes(array, value) for i = 1, table.maxn(array) do		if array[i] == value then return true end end return false end

function p.intersection(array, other) local result = {} for i, v in ipairs(array) do		if p.keyOf(other, v) then table.insert(result, v)		end end return result end

function p.keyBy(array, iteratee) return p._keyBy(iteratee)(array) end function p._keyBy(iteratee) local mapper = h.mapper(iteratee) return function(array) local result = {} for i, v in ipairs(array) do			result[mapper(v)] = v		end return result end end

function p.map(array, iteratee) return p._map(iteratee)(array) end function p._map(iteratee) return function(array) local mappingFunction = iteratee if type(iteratee) == "string" then mappingFunction = function(val) return val[iteratee] end end local tbl2 = {} for k, v in ipairs(array) do			tbl2[k] = mappingFunction(v) end return tbl2 end end

function p.mapMultiple(iteratee) local mappingFunction = h.mapper(iteratee) return function(array) local res = {} for k, v in ipairs(array) do			res[k] = {mappingFunction(v)} end return res end end

local MAX_INT = 9000 -- arbitrary high number local MIN_INT = -9000 -- arbitrary low number

function p.max(array) array = p.padNils(MIN_INT)(array) return math.max(unpack(array)) end

function p.min(array) array = p.padNils(MAX_INT)(array) return math.max(unpack(array)) end

function p.padNils(padValue, max) return function(array) padValue = padValue or '' max = max or table.maxn(array) local result = {} for i = 1, max do			if array[i] == nil then result[i] = padValue else result[i] = array[i] end end return result end end

-- returns a copy of tbl with the elements in opposite order (not a deep copy) function p.reverse(array) local tbl2 = {} local len = #array for i = len, 1, -1 do		tbl2[len - i + 1] = array[i] end return tbl2 end

function p.slice(array, s, e)	return p._slice(s, e)(array) end function p._slice(s, e)	return function(array) local tbl2 = {} e = e or table.maxn(array) for k = s, e do			tbl2[#tbl2+1] = array[k] end return tbl2 end end

function p.tail(array) local result = {} for i = 2, #array do		table.insert(result, array[i]) end return result end

function p.takeWhile(array, iteratee) local predicate = h.predicate(iteratee) local i = 1 while i <= table.maxn(array) and predicate(array[i], i) do		i = i + 1 end return p.slice(array, 1, i - 1) end

function p.dropRightWhile(iteratee) local predicate = h.predicate(iteratee) return function(array) local result = {} local i = #array while i > 0 and predicate(array[i], i) do			i = i - 1 end return p.slice(array, 1, i)	end end

function p.unique(array) local result = {} for _, v in ipairs(array) do		if not p.find(result, p._isEqual(v)) then table.insert(result, v)		end end return result end

function p.uniqueBy(iteratee) local mapper = h.mapper(iteratee) return function(array) local result = {} for _, v in ipairs(array) do			local match = p.find(result, function(val)				return mapper(val) == mapper(v)			end) if not match then table.insert(result, v)			end end return result end end

-- Based on https://github.com/lua-stdlib/functional -- See https://lua-stdlib.github.io/lua-stdlib/modules/std.functional.html#zip function p.zip(array, emptyValue) local result = {} for outerk, innerTbl in ipairs(array) do		innerTbl = p.padNils("NIL")(innerTbl) -- to properly handle sparse arrays for k, v in ipairs(innerTbl) do			result[k] = result[k] or {} if v ~= "NIL" then result[k][outerk] = v			end end if emptyValue then for k in ipairs(result) do				result[k] = p.padNils(emptyValue, #array)(result[k]) end end end return result end

p.Schemas = { print = { singleLine = { type = "boolean", }	} }

local propertyShorthand = " shorthand" local isMatchShorthand = " shorthand"

p.Documentation = { sections = { {			heading = "Tables", section = { hasArray = { params = {"tbl"}, returns = "True if the table contains integer keys (or is empty).", cases = { {							args = , expect = true, },						{							args = – , expect = true, },						{							args = , expect = false, },						{							args = , expect = true, },					}				},				isArray = { params = {"tbl"}, returns = "True if the table contains only integer keys (or is empty).", cases = { {							args = , expect = true, },						{							args = – , expect = true, },						{							args = , expect = true, },						{							args = , expect = true, },						{							args = , expect = false, },						{							args = , expect = false, },					},				},				isEmpty = { params = {"tbl"}, returns = "True if  has no keys whatsoever", cases = { {							args = – , expect = true, },						{							args = , expect = false, },						{							args = , expect = false, },					}				},				isEqual = { params = {"tbl", "other"}, _params = {{"tbl"}, {"other"}}, returns = " if   deep equals  .", cases = { {							args = { { foo = { bar = "baz" } }, { foo = { bar = "baz" } }, },							expect = true, },						{							args = { { foo = { bar = "baz" } }, { foo = { bar = "quux" } }, },							expect = false, },					}				},				invert = { params = {"tbl"}, returns = "A table with the values of  as its keys, and vice-versa.", cases = { {							args = , expect = { foo = 1, bar = 2, quux = "baz" },						},						{							desc = "Values will be overwritten if  has duplicate values. Overwrite order is not guaranteeed.", args = , expect = { Power = "Ganondorf", Courage = "Link", Wisdom = "Zelda", },						},					},				},				isMatch = { desc = "Performs a partial deep comparison between  and   to determine if   contains equivalent values.", params = {"tbl", "source"}, _params = {{"source"}, {"tbl"}}, returns = "Returns  if   is a match, else false", cases = { {							args = { { foo = { bar = "baz", flip = "flop" } }, { foo = { bar = "baz" } }, },							expect = true, },						{							args = { {1, 2, 3},								{1, 2},							},							expect = true, },						{							args = { { foo = { bar = "baz" } }, { foo = { bar = "quux" } }, },							expect = false },					},				},				keyOf = { params = {"tbl", "value"}, returns = "First key found whose value is shallow-equal to, or nil if none found.", cases = { outputOnly = true, {							args = {{"foo", nil, "bar"}, "bar"}, expect = 3, },						{							args = {{foo = "bar", baz = "quux"}, "quux"}, expect = "baz", },						{							args = {{"foo", "bar"}, "quux"}, expect = nil },						{							desc = "Does not perform deep-equal checks on tables.", args = {{{}, {}}, {}}, expect = nil },					},				},				keys = { desc = "See also and .", params = {"tbl"}, returns = "Array of  keys.", cases = { {							args = , expect = {1, 2, 3}, },						{							args = , expect = {"foo", "baz"}, },						{							args = , expect = {1, 3, "bar"} },					},				},				mapValues = { params = {"tbl", "iteratee"}, returns = "Creates a table with the same keys as  and values generated by running each value of   thru iteratee.", cases = { {							snippet = 1, expect = { arg1 = "foo", arg2 = "bar"} },						{							desc = propertyShorthand, args = { {									Link = { Triforce = "Courage", },									Zelda = { Triforce = "Wisdom" },								},								"Triforce", },							expect = { Link = "Courage", Zelda = "Wisdom", },						},					},				},				merge = { desc = "Recursively merges tables.", params = {"tbl", "..." },					returns = " with merged values. Subsequent sources overwrite key assignments of previous sources.", cases = { {							snippet = 1, expect = { flib = "flob", foo = { bar = {"noot", "flob"}, baz = "noot", wibble = "wobble", },							}						},						{							desc = "Common use: merging keys into new table.", args = { {}, 								{ flib = "flob" }, { wibble = "wobble" }, },							expect = { flib = "flob", wibble = "wobble", },						},					}				},				print = { params = {"tbl", "singleLine"}, returns = " pretty-printed as a string.", cases = { outputOnly = true, {							desc = "Prints array items on a single line.", args = , expect = '{"foo", "bar", "baz"}', },						{							desc = "Prints single line when tables has one string key.", args = , expect = '{ foo = "bar" }', },						{							args = , expect = '{1, 2, 3, foo = "bar"}', },						{							desc = "Prints one value per line if more than one string key.", args = , expect = {							 flib = "flub",							  foo = "bar",							}, },						{							args = , expect = {							 1,							  2,							  3,							  flib = "flub",							  foo = "bar",							} },						{							desc = "Prints one value per line if any are tables.", args = , expect = {							 {1},							  {2},							}, },						{							desc = string.format("Prints one value per line if single-line would exceed %s characters.", MAX_SINGLELINE), args = , expect = {							 "abcdefghijklmnopqrstuvwxyz",							  "abcdefghijklmnopqrstuvwxyz",							} },						{							desc = "Always prints single-line when  is true", args = {{"abcdefghijklmnopqrstuvwxyz", "abcdefghijklmnopqrstuvwxyz"}, true}, expect = '{"abcdefghijklmnopqrstuvwxyz", "abcdefghijklmnopqrstuvwxyz"}' },					},				},				printPath = { params = {"path"}, returns = "Property path as a string.", cases = { outputOnly = true, {							args = , expect = ".foo[1].bar[1]", },						{							args = , expect = '.foo["bar"]', },					},				},				property = { params = {"tbl", "path"}, _params = {{"path"}, {"tbl"}}, returns = "The value at  for  .", cases = { outputOnly = true, {							args = {{ foo = {"bar"} }, {"foo", 1}}, expect = "bar", },						{							args = {{ foo = {"bar"} }, {"foo", "bar", "baz"}}, expect = nil, },					},				},				size = { params = {"tbl"}, returns = "Total number of keys in .", cases = { {							args = , expect = 3, },					},				},				stringKeys = { params = {"tbl"}, returns = "Array of string keys in ", cases = { {							args = , expect = {"foo"}, },					},				},				ivalues = { params = {"tbl"}, returns = "Array of integer-keyed values in .", cases = { {							args = , expect = {"foo", "bar", "baz"}, },					},				},			},		},		{			heading = "Arrays", section = { compact = { params = {"array"}, returns = "Creates an array with falsey values removed.", cases = { {							args = , expect = {0, 1, ""} },					},				},				concat = { params = {"array", "..."}, returns = "Creates a new array concatenating array with any additional arrays and/or values.", cases = { {							snippet = 1, expect = {1, 2, 3, 4, {5}, 6, 10}, },						{							desc = "Tables with string keys are treated as single values.", args = {{}, {1, 2}, {3, 4, foo = "bar"}}, expect = {1, 2, {3, 4, foo = "bar"}}, },					},				},				difference = { params = {"array", "other"}, returns = "An array of all the elements in  that are not in  .", cases = { {							args = {{"a", "b", "c"}, {"c", "d"}}, expect = {"a", "b"}, },						{							desc = "Does not deep-compare.", args = {, }, expect = , }					},				},				keyBy = { params = {"array", "iteratee"}, _params = {{"iteratee"}, {"array"}}, returns = "Creates a table composed of keys generated from the results of running each element of  thru  ", cases = { {							snippet = 1, expect = { ["TWW Link"] = { name = "Link", game = "TWW", age = 10, },								["TP Link"] = { name = "Link", game = "TP", age = 17, },							},						},						{							desc = propertyShorthand, args = {{ {									name = "Link", age = 10, },								{									name = "Zelda", age = 10, },								{									name = "Zelda", age = 17, },							}, "name"}, expect = { ["Link"] = { name = "Link", age = 10 },								["Zelda"] = { name = "Zelda", age = 17, },							}						},					},				},				filter = { params = {"array", "iteratee"}, _params = {{"iteratee"}, {"array"}}, returns = "Iterates over array elements in, returning an array of all elements   returns truthy for.", cases = { {							snippet = 1, expect = {"foo", "bar"}, },						{							desc = propertyShorthand, args = { {									{ game = "The Wind Waker", canon = true }, { game = "Twilight Princess", canon = true }, { game = "Tingle's Rosy Rupeeland", canon = false }, },								"canon", },							expect = { { game = "The Wind Waker", canon = true }, { game = "Twilight Princess", canon = true }, },						},						{							desc = isMatchShorthand, args = { {									{ game = "The Wind Waker", type = "main" }, { game = "Twilight Princess", type = "main" }, { game = "Tingle's Rosy Rupeeland", type = "spinoff" }, },								{ type = "main" }, },							expect = { { game = "The Wind Waker", type = "main" }, { game = "Twilight Princess", type = "main" }, },						},					},				},				find = { params = {"tbl", "iteratee"}, _params = {{"iteratee"}, {"tbl"}}, returns = {"The value if found, else ", "The index of the value found, else  "}, cases = { outputOnly = true, {							snippet = 1, expect = {"bar", 2} },						{							snippet = 2, expect = {nil, nil}, },						{							desc = propertyShorthand, args = { {									{ game = "Tingle's Rosy Rupeeland", canon = false }, { game = "The Wind Waker", canon = true }, { game = "Breath of the Wild", canon = true }, },								"canon" },							expect = { { game = "The Wind Waker", canon = true }, 2,							},						},						{							desc = isMatchShorthand, args = { {									{ 										game = "Tingle's Rosy Rupeeland", canon = false, type = "spinoff", },									{										game = "Twilight Princess HD", canon = true, type = "remake", },									{ 										game = "Breath of the Wild", canon = true, type = "main" },								},								{ canon = true, type = "main" } },							expect = { { 									game = "Breath of the Wild", canon = true, type = "main" },								3							}						}					}				},				findIndex = { params = {"tbl", "iteratee"}, returns = "The index of the value if found, else ", cases = { outputOnly = true, {							snippet = 1, expect = 2 },						{							snippet = 2, expect = nil, },						{							desc = propertyShorthand, args = { {									{ game = "Tingle's Rosy Rupeeland", canon = false }, { game = "The Wind Waker", canon = true }, { game = "Breath of the Wild", canon = true }, },								"canon" },							expect = 2, },						{							desc = isMatchShorthand, args = { {									{ 										game = "Tingle's Rosy Rupeeland", canon = false, type = "spinoff", },									{										game = "Twilight Princess HD", canon = true, type = "remake", },									{ 										game = "Breath of the Wild", canon = true, type = "main" },								},								{ canon = true, type = "main" } },							expect = 3 }					},				},				findLast = { params = {"array", "iteratee"}, _params = {{"iteratee"}, {"array"}}, returns = { "The last value found matching, else  .", "The index of the value, else ." },					cases = { outputOnly = true, {							snippet = 1, expect = {"baz", 3}, },						{							snippet = 2, expect = {nil, nil}, },						{							desc = propertyShorthand, args = { {									{ game = "Tingle's Rosy Rupeeland", canon = false }, { game = "The Wind Waker", canon = true }, { game = "Breath of the Wild", canon = true }, },								"canon" },							expect = { { game = "Breath of the Wild", canon = true }, 3,							},						},						{							desc = isMatchShorthand, args = { {									{ 										game = "Tingle's Rosy Rupeeland", canon = false, type = "spinoff", },									{										game = "Twilight Princess", canon = true, type = "main", },									{ 										game = "Breath of the Wild", canon = true, type = "main" },								},								{ canon = true, type = "main" } },							expect = { { 									game = "Breath of the Wild", canon = true, type = "main" },								3,							},						},					},				},				flatten = { params = {"array"}, returns = "Returns  but flattened a single level deep.", cases = { {							args = , expect = {1, 2, {3, 4}, 5} },					},				},				flatMap = { params = {"array", "iteratee"}, _params = {{"iteratee"}, {"array"}}, returns = "A flattened array of values created by running each element in  thru   and flattening the mapped results.", cases = { {							snippet = 1, expect = {1, 1, 2, 2}, },					},				},				groupBy = { params = {"array", "iteratee"}, _params = {{"iteratee"}, {"array"}}, returns = "A table composed of keys generated from the results of running each element of  thru  . The order of grouped values is determined by the order they occur in  . The corresponding value of each key is an array of elements responsible for generating the key.", cases = { {							snippet = 1, expect = { [6] = {6.1, 6.3},								[4] = {4.2},							},						},						{							desc = propertyShorthand, args = { {									{										page = "Link", term = "Hero of Time", game = "OoT", },									{										page = "Link", term = "Hero of Winds", game = "TWW" },									{										page = "Princess Zelda", term = "Zelda", game = "SS", },								},								"page", },							expect = { ["Link"] = { {										page = "Link", term = "Hero of Time", game = "OoT", },									{										page = "Link", term = "Hero of Winds", game = "TWW" },								},								["Princess Zelda"] = { {										page = "Princess Zelda", term = "Zelda", game = "SS", },								},							},						},					},				},				includes = { params = {"array", "value"}, returns = "True if and only if  is in  .", cases = { {							args = {{"foo", "bar"}, "foo"}, expect = true },						{							args = {{"foo", "bar"}, "baz"}, expect = false },					},				},				intersection = { params = {"array", "other"}, returns = "An array of all the elements that are in both  and  ", cases = { {							args = {{"a", "b", "c"}, {"b"}}, expect = {"b"}, },						{							desc = "Does not deep-compare.", args = {, }, expect = {}, },					},				},				map = { params = {"array", "iteratee"}, _params = {{"iteratee"}, {"array"}}, returns = "Creates an array of values by running each array element in  thru  .", cases = { {							snippet = 1, expect = {"true", "false"}, },						{							desc = propertyShorthand, args = { {									{										name = "Link", triforce = "Courage", },									{										name = "Zelda", triforce = "Wisdom", },									{										name = "Ganon", triforce = "Power" },								},								"triforce" },							expect = {"Courage", "Wisdom", "Power"}, },					},				},			}		},	}, }

return p