Modul:Ewige Gegner-Scorer gegen Kassel

Version vom 13. Mai 2026, 13:11 Uhr von ChatBot (Diskussion | Beiträge) (Dynamische VS-Kassel-Spielseitenauswertung ergänzt)

Die Dokumentation für dieses Modul kann unter Modul:Ewige Gegner-Scorer gegen Kassel/Doku erstellt werden

local p = {}

local TEAM_CODES = {
	'AEV', 'BCP', 'BDP', 'BHV', 'BIE', 'BTT', 'DEG', 'DGF', 'DOR', 'DRE', 'DUI',
	'EBB', 'EBR', 'ECN', 'ESS', 'ESVK', 'ETC', 'EVL', 'FRA', 'FRB', 'HAL', 'HER',
	'HHF', 'HNF', 'HSC', 'IEC', 'ING', 'KEC', 'KRE', 'LFX', 'MAN', 'MUB', 'MUC',
	'NBG', 'RAT', 'RAV', 'RLO', 'SBR', 'SCM', 'SCR', 'SEL', 'SWW', 'TIM', 'TÖL',
	'WEI', 'WOB'
}

local function trim(value)
	return mw.text.trim(value or '')
end

local function getArg(frame, name)
	local value = trim(frame.args[name])
	if value ~= '' then
		return value
	end

	local parent = frame:getParent()
	if parent then
		return trim(parent.args[name])
	end

	return ''
end

local function splitList(value)
	local result = {}
	for item in trim(value):gmatch('[^,]+') do
		item = trim(item)
		if item ~= '' then
			table.insert(result, item)
		end
	end
	return result
end

local function uniqueInsert(list, seen, value)
	value = trim(value)
	if value ~= '' and not seen[value] then
		seen[value] = true
		table.insert(list, value)
	end
end

local function escapePattern(value)
	return (value:gsub('([%(%)%.%%%+%-%*%?%[%]%^%$])', '%%%1'))
end

local function stripPlayerSuffix(title)
	title = trim(title)
	title = title:gsub('%s+%(Gastspieler.-%)$', '')
	title = title:gsub('%s+%(Spieler.-%)$', '')
	title = title:gsub('%s+%(Torhüter.-%)$', '')
	title = title:gsub('%s+%(Stürmer.-%)$', '')
	title = title:gsub('%s+%(Verteidiger.-%)$', '')
	return trim(title)
end

local function germanize(text)
	local replacements = {
		['Ä'] = 'Ae', ['ä'] = 'ae',
		['Ö'] = 'Oe', ['ö'] = 'oe',
		['Ü'] = 'Ue', ['ü'] = 'ue',
		['ß'] = 'ss'
	}

	for from, to in pairs(replacements) do
		text = mw.ustring.gsub(text, from, to)
	end

	return text
end

local function latinize(text)
	local replacements = {
		['Ä'] = 'A', ['ä'] = 'a',
		['Ö'] = 'O', ['ö'] = 'o',
		['Ü'] = 'U', ['ü'] = 'u',
		['ß'] = 'ss',
		['Á'] = 'A', ['á'] = 'a',
		['À'] = 'A', ['à'] = 'a',
		['Â'] = 'A', ['â'] = 'a',
		['Č'] = 'C', ['č'] = 'c',
		['Ć'] = 'C', ['ć'] = 'c',
		['Ď'] = 'D', ['ď'] = 'd',
		['É'] = 'E', ['é'] = 'e',
		['Ě'] = 'E', ['ě'] = 'e',
		['Í'] = 'I', ['í'] = 'i',
		['Ĺ'] = 'L', ['ĺ'] = 'l',
		['Ľ'] = 'L', ['ľ'] = 'l',
		['Ň'] = 'N', ['ň'] = 'n',
		['Ó'] = 'O', ['ó'] = 'o',
		['Ô'] = 'O', ['ô'] = 'o',
		['Ř'] = 'R', ['ř'] = 'r',
		['Š'] = 'S', ['š'] = 's',
		['Ť'] = 'T', ['ť'] = 't',
		['Ú'] = 'U', ['ú'] = 'u',
		['Ů'] = 'U', ['ů'] = 'u',
		['Ý'] = 'Y', ['ý'] = 'y',
		['Ž'] = 'Z', ['ž'] = 'z'
	}

	for from, to in pairs(replacements) do
		text = mw.ustring.gsub(text, from, to)
	end

	return text
end

local function makeTextVariants(text)
	local base = trim((text or ''):gsub('_', ' '))
	local variants = {}
	local seen = {}
	uniqueInsert(variants, seen, base)
	uniqueInsert(variants, seen, germanize(base))
	uniqueInsert(variants, seen, latinize(base))
	return variants
end

local function getRawPagesInCategory(frame, categoryName)
	categoryName = trim(categoryName)
	if categoryName == '' then
		return 0
	end

	local ok, count = pcall(function()
		return mw.site.stats.pagesInCategory(categoryName, 'pages')
	end)
	if ok and type(count) == 'number' then
		return count
	end

	local raw = frame:preprocess('{{SEITEN_IN_KATEGORIE:' .. categoryName .. '|pages}}')
	return tonumber(trim(raw)) or 0
end

local function getCategoryMembers(category)
	if not (mw.ext and mw.ext.dpl and type(mw.ext.dpl.getPagenames) == 'function') then
		return {}
	end

	local ok, pages = pcall(mw.ext.dpl.getPagenames, {
		category = trim(category):gsub('^Kategorie:%s*', ''),
		namespace = 'main',
		redirects = 'exclude',
		ordermethod = 'sortkey',
		order = 'ascending',
		count = 5000
	})

	if not ok then
		return {}
	end
	return pages or {}
end

local function getPageContent(pageName)
	local title = mw.title.new(pageName)
	if not title then
		return ''
	end
	local ok, content = pcall(function()
		return title:getContent()
	end)
	if not ok then
		return ''
	end
	return content or ''
end

local function addSourcePlayers(players, seen, sourceCategories)
	for _, category in ipairs(splitList(sourceCategories)) do
		for _, pageName in ipairs(getCategoryMembers(category)) do
			local name = stripPlayerSuffix(pageName)
			if name ~= '' and not seen[name] then
				seen[name] = { page = pageName, name = name }
				table.insert(players, seen[name])
			end
		end
	end
end

local function addExtraPlayers(players, seen, extraPlayers)
	for _, name in ipairs(splitList(extraPlayers)) do
		if not seen[name] then
			seen[name] = { page = name, name = name }
			table.insert(players, seen[name])
		end
	end
end

local function makeCategoryName(season, value, statKey, playerName, competition)
	local head
	if statKey == 'GP' then
		head = trim(season .. ' GP ' .. playerName .. ' VS-Kassel')
	else
		head = trim(season .. ' ' .. tostring(value) .. ' ' .. statKey .. ' ' .. playerName .. ' VS-Kassel')
	end
	if trim(competition) ~= '' then
		head = head .. ' ' .. trim(competition)
	end
	return head
end

local function countFirstVariant(frame, names)
	for _, name in ipairs(names) do
		local count = getRawPagesInCategory(frame, name)
		if count > 0 then
			return count
		end
	end
	return 0
end

local function countPlayerCategory(frame, season, value, statKey, playerName, competition)
	local candidates = {}
	local seen = {}
	for _, variant in ipairs(makeTextVariants(playerName)) do
		uniqueInsert(candidates, seen, makeCategoryName(season, value, statKey, variant, competition))
	end
	return countFirstVariant(frame, candidates)
end

local function sumStat(frame, seasons, competitions, playerName, statKey, maxValue)
	local total = 0
	for _, season in ipairs(seasons) do
		for _, competition in ipairs(competitions) do
			for value = 1, maxValue do
				local count = countPlayerCategory(frame, season, value, statKey, playerName, competition)
				total = total + (value * count)
			end
		end
	end
	return total
end

local function sumGames(frame, seasons, competitions, playerName)
	local total = 0
	for _, season in ipairs(seasons) do
		for _, competition in ipairs(competitions) do
			total = total + countPlayerCategory(frame, season, 0, 'GP', playerName, competition)
		end
	end
	return total
end

local function addTeam(target, code)
	code = trim(code)
	if code == '' then
		return
	end
	for _, existing in ipairs(target.teams) do
		if existing == code then
			return
		end
	end
	table.insert(target.teams, code)
end

local function addDynamicTeams(frame, row, seasons, competitions, playerName)
	for _, season in ipairs(seasons) do
		for _, competition in ipairs(competitions) do
			for _, code in ipairs(TEAM_CODES) do
				local head = trim(season .. ' Team ' .. code .. ' ' .. playerName .. ' VS-Kassel')
				if competition ~= '' then
					head = head .. ' ' .. competition
				end
				if getRawPagesInCategory(frame, head) > 0 then
					addTeam(row, code)
				end
			end
		end
	end
end

local function rowKey(row)
	if trim(row.page) ~= '' then
		return 'page:' .. mw.ustring.lower(trim(row.page))
	end
	return 'name:' .. mw.ustring.lower(trim(row.name))
end

local function cloneBaseRow(row)
	local cloned = {
		page = trim(row.page),
		name = trim(row.name),
		games = tonumber(row.games) or 0,
		gamesUnknown = row.gamesUnknown and true or false,
		goals = tonumber(row.goals) or 0,
		assists = tonumber(row.assists) or 0,
		points = tonumber(row.points) or 0,
		teams = {}
	}
	for _, code in ipairs(row.teams or {}) do
		addTeam(cloned, code)
	end
	return cloned
end

local function mergeRows(baseRows, dynamicRows)
	local rows = {}
	local byKey = {}

	for _, row in ipairs(baseRows) do
		local cloned = cloneBaseRow(row)
		local key = rowKey(cloned)
		byKey[key] = cloned
		table.insert(rows, cloned)
	end

	for _, row in ipairs(dynamicRows) do
		local key = rowKey(row)
		local target = byKey[key]
		if not target then
			target = cloneBaseRow(row)
			byKey[key] = target
			table.insert(rows, target)
		else
			target.games = target.games + (tonumber(row.games) or 0)
			target.goals = target.goals + (tonumber(row.goals) or 0)
			target.assists = target.assists + (tonumber(row.assists) or 0)
			target.points = target.points + (tonumber(row.points) or 0)
			for _, code in ipairs(row.teams or {}) do
				addTeam(target, code)
			end
		end
	end

	table.sort(rows, function(a, b)
		if a.points ~= b.points then return a.points > b.points end
		if a.goals ~= b.goals then return a.goals > b.goals end
		if a.assists ~= b.assists then return a.assists > b.assists end
		if a.games ~= b.games then return a.games > b.games end
		return mw.ustring.lower(a.name) < mw.ustring.lower(b.name)
	end)

	return rows
end

local function collectDynamicRows(frame, baseRows, dynamicSeasons, competitions, sourceCategories, extraPlayers, maxPoints, maxGoals, maxAssists)
	local seasons = splitList(dynamicSeasons)
	if #seasons == 0 then
		return {}
	end

	local rowsByName = {}
	local orderedRows = {}
	local scannedPages = {}

	local function ensureRow(name)
		name = trim(name)
		local row = rowsByName[name]
		if not row then
			row = {
				page = name,
				name = name,
				games = 0,
				goals = 0,
				assists = 0,
				points = 0,
				teams = {}
			}
			rowsByName[name] = row
			table.insert(orderedRows, row)
		end
		return row
	end

	for _, season in ipairs(seasons) do
		for _, competition in ipairs(competitions) do
			local seasonPattern = escapePattern(season)
			local competitionPattern = escapePattern(competition)
			for _, pageName in ipairs(getCategoryMembers(season .. ' ' .. competition)) do
				if not scannedPages[pageName] then
					scannedPages[pageName] = true
					local content = getPageContent(pageName)
					for category in content:gmatch('%[%[%s*[Kk]ategorie%s*:%s*([^%]]+)%]%]') do
						category = trim(category)
						local gpName = category:match('^' .. seasonPattern .. ' GP (.-) VS%-Kassel ' .. competitionPattern .. '$')
						if gpName then
							local row = ensureRow(gpName)
							row.games = row.games + 1
						end

						local value, statKey, statName = category:match('^' .. seasonPattern .. ' (%d+) ([TAP]) (.-) VS%-Kassel ' .. competitionPattern .. '$')
						if value and statKey and statName then
							local row = ensureRow(statName)
							value = tonumber(value) or 0
							if statKey == 'T' then
								row.goals = row.goals + value
							elseif statKey == 'A' then
								row.assists = row.assists + value
							elseif statKey == 'P' then
								row.points = row.points + value
							end
						end

						local teamCode, teamName = category:match('^' .. seasonPattern .. ' Team ([A-ZÄÖÜ0-9]+) (.-) VS%-Kassel ' .. competitionPattern .. '$')
						if teamCode and teamName then
							addTeam(ensureRow(teamName), teamCode)
						end
					end
				end
			end
		end
	end

	if #orderedRows > 0 then
		return orderedRows
	end

	local playerEntries = {}
	local seen = {}
	for _, row in ipairs(baseRows) do
		local name = trim(row.name)
		if name ~= '' and not seen[name] then
			seen[name] = { page = trim(row.page), name = name }
			table.insert(playerEntries, seen[name])
		end
	end
	addSourcePlayers(playerEntries, seen, sourceCategories)
	addExtraPlayers(playerEntries, seen, extraPlayers)

	local rows = {}
	for _, player in ipairs(playerEntries) do
		local games = sumGames(frame, seasons, competitions, player.name)
		local goals = sumStat(frame, seasons, competitions, player.name, 'T', maxGoals)
		local assists = sumStat(frame, seasons, competitions, player.name, 'A', maxAssists)
		local points = sumStat(frame, seasons, competitions, player.name, 'P', maxPoints)
		if games > 0 or goals > 0 or assists > 0 or points > 0 then
			local row = {
				page = player.page,
				name = player.name,
				games = games,
				goals = goals,
				assists = assists,
				points = points,
				teams = {}
			}
			addDynamicTeams(frame, row, seasons, competitions, player.name)
			table.insert(rows, row)
		end
	end
	return rows
end

local function formatName(row)
	if trim(row.page) ~= '' then
		return string.format('[[%s|%s]]', row.page, row.name)
	end
	return row.name
end

local function formatTeams(row)
	local out = {}
	for _, code in ipairs(row.teams or {}) do
		table.insert(out, '[[' .. code .. ']]')
	end
	return table.concat(out, ' · ')
end

local function formatGames(row)
	if row.gamesUnknown then
		return '-'
	end
	return tostring(row.games or 0)
end

local function formatAverage(row)
	if row.gamesUnknown or not row.games or row.games <= 0 then
		return '-'
	end
	return string.format('%.2f', (row.points or 0) / row.games)
end

function p.render(frame)
	local baseDataModule = getArg(frame, 'baseDataModule')
	if baseDataModule == '' then
		baseDataModule = 'Ewige Gegner-Scorer gegen Kassel/Daten/Basis'
	end

	local ok, data = pcall(mw.loadData, 'Modul:' .. baseDataModule)
	if not ok or type(data) ~= 'table' then
		return '<strong>Fehler:</strong> Basisdaten konnten nicht geladen werden.'
	end

	local caption = getArg(frame, 'caption')
	if caption == '' then
		caption = 'Ewige Gegner-Scorer gegen Kassel'
	end

	local competitions = splitList(getArg(frame, 'competition'))
	if #competitions == 0 then
		competitions = { 'HR', 'PO' }
	end

	local maxPoints = tonumber(getArg(frame, 'maxPoints')) or 8
	local maxGoals = tonumber(getArg(frame, 'maxGoals')) or 6
	local maxAssists = tonumber(getArg(frame, 'maxAssists')) or 6
	local sourceCategories = getArg(frame, 'sourceCategories')
	if sourceCategories == '' then
		sourceCategories = 'Spieler'
	end

	local dynamicRows = collectDynamicRows(
		frame,
		data.rows or {},
		getArg(frame, 'dynamicSeasons'),
		competitions,
		sourceCategories,
		getArg(frame, 'extraPlayers'),
		maxPoints,
		maxGoals,
		maxAssists
	)
	local rows = mergeRows(data.rows or {}, dynamicRows)

	local lines = {
		'{| class="wikitable plainrowheaders sortable static-row-numbers"',
		'|+ ' .. caption,
		'! Name !! Teams !! Spiele !! Tore !! Assists !! Punkte !! Ø Pkt.'
	}

	for _, row in ipairs(rows) do
		table.insert(lines, '|-')
		table.insert(lines, '! scope="row" | ' .. formatName(row))
		table.insert(lines, '| ' .. formatTeams(row))
		table.insert(lines, '| ' .. formatGames(row))
		table.insert(lines, '| ' .. tostring(row.goals or 0))
		table.insert(lines, '| ' .. tostring(row.assists or 0))
		table.insert(lines, '| ' .. tostring(row.points or 0))
		table.insert(lines, '| ' .. formatAverage(row))
	end

	table.insert(lines, '|}')
	return table.concat(lines, '\n')
end

return p