Module:UtilsTable

local p = {} local h = {}

local utilsNumber = require("Module:UtilsNumber")

-- 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.clone(val) if type(val) ~= "table" then return val end local result = {} for k, v in pairs(val) do		result[k] = v	end return result end

function p.cloneDeep(val) if type(val) ~= "table" then return val end return p.merge({}, val) end

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

function p.hasKey(tbl, key) return p._hasKey(key)(tbl) end function p._hasKey(key) return function(tbl) return not not tbl[key] end 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 result = {} for _, v in ipairs(tbl) do		table.insert(result, v)	end return result 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 result = {} for k in pairs(tbl) do  		h.append(result, k)   end return result 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] = p.clone(v) end end end return tbl end

local inspect local MAX_SINGLELINE = 50 -- If changing this, also change the variable of the same name at Module:UtilsTable/Documentation/Data function p.print(tbl, singleLine) inspect = inspect or require("Module:UtilsTable/Inspect") -- lazy-loaded for performance optimization 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

function p.toArray(tbl, keys, padValue) return p._toArray(keys, padValue)(tbl) end function p._toArray(keys, padValue) return function(tbl) local array = {} if keys then for i, key in ipairs(keys) do				array[i] = tbl[key] or padValue end else for k, v in pairs(tbl) do				table.insert(array, v)			end end return array end 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, ...) if type(array) ~= "table" then array = {array} end local result = p.clone(array) for i, arrayOrValue in ipairs({...}) do		if not p.hasArray(arrayOrValue) then h.append(result, arrayOrValue) else for _, value in ipairs(arrayOrValue) do				h.append(result, value) 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.dropRightWhile(array, predicate) return p._dropRightWhile(predicate)(array) end function p._dropRightWhile(predicate) local predicate = h.predicate(predicate) return function(array) local result = {} local i = p.len(array) while i > 0 and predicate(array[i], i) do			i = i - 1 end while i > 0 do			table.insert(result, 1, array[i]) i = i - 1 end return result end 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, v in ipairs(array) do			if predicateFn(v) then table.insert(results, v)			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, p.len(array) - i + 1 end end

function p.flatten(array) local result = {} for _, v in ipairs(array) do		result = p.concat(result, v)	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 _, v in ipairs(array) do		if v == 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.len(array) return #p.clone(array) 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.min(array) local min for _, v in ipairs(array) do		min = math.min(min or utilsNumber.MAX, v)	end return min end

function p.max(array) local max for _, v in ipairs(array) do		max = math.max(max or utilsNumber.MIN, v)	end return max end

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

function p.partition(array, iteratee) return p._partition(iteratee)(array) end function p._partition(iteratee) return function(array) local predicateFn = h.predicate(iteratee) local trueResults = {} local falseResults = {} for i, v in ipairs(array) do			if predicateFn(v) then table.insert(trueResults, v)			else table.insert(falseResults, v)			end end return trueResults, falseResults 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 = p.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 p.len(array) local j = 1 for i = s, e do			tbl2[j] = array[i] i = i + 1 j = j + 1 end return tbl2 end end

function p.sortBy(array, iteratees) return p._sortBy(iteratees)(array) end function p._sortBy(iteratees) if type(iteratees) ~= "table" then iteratees = {iteratees} end local comparators = {} for i, iteratee in ipairs(iteratees) do		local mappingFn = h.mapper(iteratee) comparators[i] = h.comparator(mappingFn) end return function(array) local result = p.clone(array) table.sort(result, function(a, b)			local isEqual, isLessThan			local i = 1			repeat				isEqual, isLessThan = comparators[i](a, b)				i = i + 1			until not isEqual or i > #iteratees			return isLessThan		end) return result end end function h.comparator(mappingFn) return function(a, b)		a = mappingFn(a) b = mappingFn(b) return a == b, a < b	end end

function p.tail(array) local result = {} for i in ipairs(array) do		if i > 1 then result[i - 1] = array[i] end end return result end

function p.take(array, n)	return p._take(n)(array) end function p._take(n) return function(array) return p.slice(array, 1, n)	end end

function p.takeWhile(array, predicate) return p._takeWhile(predicate)(array) end function p._takeWhile(predicate) local predicate = h.predicate(predicate) return function(array) local result = {} local i = 1 while i <= p.len(array) and predicate(array[i], i) do			result[i] = array[i] i = i + 1 end return result end end

function p.union(arrays) local result = {} local seen = {} for _, array in ipairs(arrays) do		for _, value in ipairs(array) do			if not seen[value] then table.insert(result, value) seen[value] = true end end end return result 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(array, iteratee) return p._uniqueBy(iteratee)(array) 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, padValue) local result = {} local len = p.len(array) for outerk, innerTbl in ipairs(array) do		innerTbl = p.padNils(innerTbl, "NIL") -- 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 padValue then for k in ipairs(result) do				result[k] = p.padNils(result[k], padValue, len) end end end return result end

return p