Module:CargoUtil

From Leaguepedia | League of Legends Esports Wiki
Jump to: navigation, search

To edit the documentation or categories for this module, click here.


local util_args = require('Module:ArgsUtil')
local util_map = require('Module:MapUtil')
local util_sort = require('Module:SortUtil')
local util_table = require('Module:TableUtil')
local util_text = require('Module:TextUtil')
local util_vars = require('Module:VarsUtil')
local bool_false = { ['false'] = true, ['0'] = true, ['no'] = true, [''] = true }
local argPrefix = 'q?'
local lang = mw.getLanguage('en')
local bool_to_str = { [true] = 'Yes', [false] = 'No' }

local p = {}
local h = {}

-- oneToMany structure:
--[[
	{
		groupBy = { set of things to group by here },
		fields = {
			key1_plural = singleFieldSingular,
			key2_plural = { table, of, fields },
		}
	}
]]

function p.queryAndCast(query)
	local copyQuery = h.finalizeQuery(query)
	local result = mw.ext.cargo.query(
		copyQuery.tables,
		copyQuery.fields,
		copyQuery
	)
	h.cast(result, copyQuery.types or {})
	return h.groupOneToManyFields(result, copyQuery.oneToMany)
end

function h.finalizeQuery(query)
	local copyQuery = mw.clone(query)
	copyQuery.tables = util_table.concatIfTable(query.tables)
	copyQuery.fields = h.parseAndConcatFieldNames(query.fields, query.oneToMany)
	copyQuery.join = util_table.concatIfTable(query.join)
	copyQuery.limit = query.limit or 9999
	-- default to the where being an AND of its params if it's a table
	if type(query.where) == 'table' then
		copyQuery.where = p.concatWhere(query.where)
	end
	return copyQuery
end

function h.parseAndConcatFieldNames(fields, oneToMany)
	if not oneToMany then oneToMany = {} end
	oneToMany.allFields = h.getListOfAllOneToManyFields(oneToMany.fields)
	if type(fields) == 'string' then
		fields = util_text.split(fields)
	end
	util_table.mergeArrays(fields, oneToMany.allFields)
	return util_table.concat(fields, ',', h.parseOneFieldName)
end

function h.getListOfAllOneToManyFields(fields)
	if not fields then return nil end
	local allFields = {}
	for k, v in pairs(fields) do
		util_table.merge(allFields, util_table.guaranteeTable(v))
	end
	return allFields
end

function h.parseOneFieldName(str)
	if not str:find('%.') then
		return str
	elseif str:find('=') then
		return str
	end
	local name = str:match('%.(.+)')
	return ('%s=%s'):format(str, name)
end

function h.cast(result, types)
	for i, row in ipairs(result) do
		row.index = i
		for k, v in pairs(row) do
			row[k] = h.castField(v, types[k])
		end
	end
end

function h.castField(v, v_type)
	if v == '' then return nil end
	if not v_type then return v end
	if v_type == 'boolean' then
		return p.strToBool(v)
	elseif v_type == 'number' then
		return tonumber(v)
	elseif v_type == 'namespace' then
		return mw.site.namespaces[tonumber(v)].name
	end
	error('Unrecognized Cargo value casting type')
end

function h.groupOneToManyFields(result, oneToMany)
	if not oneToMany then return result end
	local currentKey
	-- fields is a blob
	local fieldsBlob = h.parseFieldSetsForKeys(oneToMany.fields)
	local groupedResult = {}
	for _, row in ipairs(result) do
		local newKey = h.getNewKey(row, oneToMany.groupBy)
		if newKey == currentKey then
			h.addRowToExistingGroup(groupedResult[#groupedResult], row, fieldsBlob)
		else
			h.addRowToNewGroup(groupedResult, row, fieldsBlob)
			currentKey = newKey
		end
	end
	return groupedResult
end

function h.parseFieldSetsForKeys(fields)
	-- fields is a blob, and we return a blob
	return util_map.safe(fields, h.parseFieldsForKeys)
end

function h.parseFieldsForKeys(fieldSet)
	return util_map.inPlace(
		util_map.inPlace(
			util_table.guaranteeTable(fieldSet),
			h.parseOneFieldName
		),
		h.parseOneFieldForKey
	)
end

function h.getNewKey(row, groupBy)
	local toConcat = {}
	for _, v in ipairs(groupBy) do
		toConcat[#toConcat+1] = row[v]
	end
	return table.concat(toConcat)
end

function h.parseOneFieldForKey(str)
	if not str:find('=') then return str end
	return str:match('=(.+)')
end

function h.addRowToExistingGroup(groupedRow, row, fieldsBlob)
	for k, fieldSet in pairs(fieldsBlob) do
		util_table.push(groupedRow[k], h.extractFieldsetFromDataRow(row, fieldSet))
	end
end

function h.extractFieldsetFromDataRow(row, fieldSet)
	local ret = {}
	for _, field in ipairs(fieldSet) do
		ret[field] = row[field]
	end
	return ret
end

function h.addRowToNewGroup(groupedResult, row, fieldsBlob)
	for k, fieldSet in pairs(fieldsBlob) do
		row[k] = { h.extractFieldsetFromDataRow(row, fieldSet) }
	end
	groupedResult[#groupedResult+1] = row
end

function p.getOneResult(query, field)
	local result = p.queryAndCast(query)
	if result[1] then
		return result[1][field or h.getOneFieldName(query.fields)]
	end
	return nil
end

function h.getOneFieldName(field)
	if type(field) == 'table' then field = field[1] end
	return h.parseOneFieldForKey(h.parseOneFieldName(field))
end

function p.getOneRow(query)
	local result = p.queryAndCast(query)
	return result[1] or {}
end

function p.getOneField(query, field)
	local result = p.queryAndCast(query)
	local tbl = {}
	for i, row in ipairs(result) do
		tbl[#tbl+1] = row[field]
	end
	return tbl
end

function p.strToBool(v)
	if not v then
		return false
	elseif bool_false[lang:lc(v)] then
		return false
	end
	return true
end

function p.getConstDict(query, key, value)
	return p.makeConstDict(p.queryAndCast(query), key, value)
end

function p.makeConstDict(result, key, value)
	local tbl = {}
	for _, row in ipairs(result) do
		if row[key] then
			tbl[row[key]] = row[value]
		end
	end
	return tbl
end

function p.getRowDict(query, key)
	local result = p.queryAndCast(query)
	local ret = {}
	for _, row in ipairs(result) do
		if row[key] then
			ret[row[key]] = row
		end
	end
	return ret
end

function p.getOrderedDict(query, key, value)
	return h.makeOrderedDict(p.queryAndCast(query), key, value)
end

function h.makeOrderedDict(result, key, value)
	local tbl = {}
	for _, row in ipairs(result) do
		if row[key] then
			tbl[#tbl+1] = row[key]
			tbl[row[key]] = row[value]
		end
	end
	return tbl
end

function p.getOrderedList(query, key)
	local result = p.queryAndCast(query)
	return h.makeOrderedList(result, key or query.fields)
end

function h.makeOrderedList(result, key)
	local tbl = {}
	for k, row in ipairs(result) do
		tbl[#tbl+1] = row[key]
	end
	return tbl
end

function p.groupResultOrdered(result, key, f)
	local data = {}
	local this
	local thisvalue
	local thistab
	local i = 1
	for _, row in ipairs(result) do
		if not row[key] then row[key] = 'Uncategorized' end
		if row[key] ~= thisvalue then
			data[#data+1] = { name = row[key], index = i }
			i = i + 1
			thistab = data[#data] or {}
			thisvalue = row[key]
		end
		thistab[#thistab+1] = f and f(row) or row
	end
	return data
end	

function p.groupResultByValue(result, key, f)
	local data = {}
	local this
	local thisvalue
	local i = 1
	for _, row in ipairs(result) do
		if row[key] ~= thisvalue then
			thisvalue = row[key]
			data[thisvalue] = { name = row[key] }
			i = i + 1
			thistab = data[thisvalue]
		end
		thistab[#thistab+1] = f and f(row) or row
	end
	return data
end

function p.queryFromArgs(args, defaults)
	-- sometimes we want to specify query args in the template
	-- this function parses them into args that cargo will understand
	-- change argPrefix above to change the prefix for query params
	local query = mw.clone(defaults or {})
	for k, v in pairs(args) do
		if string.sub(k, 0, 2) == argPrefix then
			query[string.sub(k,3)] = v
		end
	end
	return query
end

function p.store(tbl)
	if CARGO_NAMESPACE and mw.title.getCurrentTitle().nsText ~= CARGO_NAMESPACE then
		return
	end
	if not tbl then return end
	local tbl2 = { '' }
	for k, v in pairs(tbl) do
		if type(v) == 'boolean' then
			tbl2[k] = bool_to_str[v]
		else
			tbl2[k] = v
		end
	end
	mw.getCurrentFrame():callParserFunction{
		name = '#cargo_store',
		args = tbl2
	}
	return
end

function p.setStoreNamespace(ns)
	CARGO_NAMESPACE = ns
end

function p.doWeStoreCargo(nocargo, desiredNamespace,title)
	local argOkay = not util_args.castAsBool(nocargo)
	if not desiredNamespace then
		return argOkay
	end
	if not title then
		title = mw.title.getCurrentTitle()
	end
	return argOkay and title.nsText == desiredNamespace
end

function p.whereFromArg(str, ...)
	-- if an arg is defined, formats a string with the arg to be included in a where table
	-- if it's not defined, returns false and NOT nil so the table can be used
	-- with util_table.concat
	if #{...} == 0 then
		return false
	else
		return str:format(...)
	end
end

function p.whereFromArgList(str, argTbl, sep, f)
	if not sep then sep = '%s*,%s*' end
	if not argTbl then return nil end
	argTbl = util_table.guaranteeTable(argTbl)
	if #argTbl == 0 then return end
	local splitArgs = {}
	for _, arg in ipairs(argTbl) do
		splitArgs[#splitArgs+1] = util_map.split(arg, sep, f)
	end
	local argsForFormat = {}
	for lineIndex, v in ipairs(splitArgs[1]) do
		argsForFormat[lineIndex] = {}
		for i, arg in ipairs(splitArgs) do
			argsForFormat[lineIndex][i] = arg[lineIndex]
		end
	end
	local where = {}
	for _, condition in ipairs(argsForFormat) do
		where[#where+1] = p.whereFromArg(str, unpack(condition))
	end
	return ('(%s)'):format(p.concatWhereOr(where))
end

function p.concatWhere(tbl)
	local arr = {}
	-- pairs because maybe some entries are nil, and since it's an AND, order doesn't matter
	for _, v in pairs(tbl) do
		if v then
			arr[#arr+1] = ('(%s)'):format(v)
		end
	end
	if #arr == 0 then return nil end
	return '(' .. util_table.concat(arr, ' AND ') .. ')'
end

function p.concatWhereOr(tbl)
	local arr = {}
	-- pairs because maybe some entries are nil, and since it's an AND, order doesn't matter
	for _, v in pairs(tbl) do
		if v then
			arr[#arr+1] = ('(%s)'):format(v)
		end
	end
	return '(' .. util_table.concat(arr, ' OR ') .. ')'
end

function p.fakeHolds(field, str, sep)
	if str == nil then return false end
	sep = sep or ','
	str = h.escape(str)
	return ('%s__full RLIKE ".*(^|%s)%s($|%s).*"'):format(field, sep, str, sep)
end

function h.escape(str)
	local tbl = { '%(', '%)' }
	for _, v in ipairs(tbl) do
		str = str:gsub(v, '.')
	end
	return str
end

function p.fakeHoldsVariable(field, str, sep)
	sep = sep or ','
	return ('%s__full RLIKE CONCAT(".*(^|%s)",%s,"($|%s).*")'):format(field, sep, str, sep)
end

function p.makeMinMaxQuery(query, field, orderby, order)
	-- modifies a pre-existing query to add an extra set of conditions to get the max/min value of some field
	-- order will be either MIN or MAX, and orderby is usually going to be a date/datetime
	-- example: c.makeMinMaxQuery(query, 'SP.Champion','SP.Time','MAX')
	--to get the most-recent played champions
	local query2 = mw.clone(query)
	query2.fields = ("%s(%s)=value, %s=field"):format(order or 'MAX', orderby, field)
	local result = p.queryAndCast(query2)
	util_map.inPlace(result, function(row)
		return row.value and ('(%s="%s" AND %s="%s")'):format(field, row.field, orderby, row.value)
	end)
	local newwhere = {
		next(result) and ("(%s)"):format(p.concatWhereOr(result)),
		query.where and ("(%s)"):format(query.where)
	}
	return p.concatWhere(newwhere)
end

function p.getUniqueLine(...)
	local args = {...}
	for k, v in ipairs(args) do
		if type(v) == 'string' then
			args[k] = util_vars.getGlobalIndex(v) or v
		end
	end
	table.insert(args, 1, mw.title.getCurrentTitle().text)
	return util_table.concat(args, '_')
end

function p.concatQueriesAnd(original, new)
	-- combine tables, fields, and join
	-- "and" the wheres together
	-- overwrite everything else with new
	for _, v in ipairs({ 'tables', 'fields', 'join' }) do
		original[v] = util_text.splitIfString(original[v])
		util_table.mergeArrays(original[v], util_text.splitIfString(new[v]))
		new[v] = nil
	end
	new.where = h.concatQueriesWhereAnd(original.where, new.where)
	util_table.merge(new.types, original.types)
	util_table.merge(original, new)
	return original
end

function h.concatQueriesWhereAnd(original, new)
	if not original then return new end
	if not new then return original end
	local tbl = { original, new }
	return p.concatWhere(tbl)
end

function p.wikitextQuery(query)
	local copyQuery = h.finalizeQuery(query)
	local text = ([[{{#cargo_query:tables=%s

|join on=%s

|fields=%s

|where=%s

|order by=%s

|group by=%s

}}]]):format(
	copyQuery.tables or '',
	copyQuery.join or '',
	copyQuery.fields or '',
	copyQuery.where or '',
	copyQuery.orderBy or '',
	copyQuery.groupBy or ''
)
	return mw.text.nowiki(text)
end

function p.logQuery(query)
	util_vars.log(p.wikitextQuery(query))
end

return p