Module:Standings

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

Documentation for this module may be created at Module:Standings/doc

local util_args = require('Module:ArgsUtil')
local util_cargo = require('Module:CargoUtil')
local util_esports = require('Module:EsportsUtil')
local util_footnotes = require('Module:FootnoteUtil')
local util_html = require('Module:HTMLUtil')
local util_math = require('Module:MathUtil')
local util_table = require('Module:TableUtil')
local util_text = require('Module:TextUtil')
local util_tournament = require('Module:TournamentUtil')
local util_vars = require('Module:VarsUtil')
local m_team = require('Module:Team')
local i18n = require('Module:I18nUtil')

local Legend = require('Module:Legend')._main
local PopupButton = require('Module:PopupButton')

local lang = mw.getLanguage('en')
local sep = '%s*,%s*'
local argsep = '%s*;;;%s*'

local PRELOADS = {
	bo1 = {
		columns = 'bo1',
		sortmethod = 'record',
	},
	bo1points = {
		columns = 'bo1points',
		sortmethod = 'points',
	},
	bo2 = {
		isbo2 = true,
		columns = 'bo2',
		sortmethod = 'bo2points'
	},
	bo2onlypoints = {
		isbo2 = true,
		columns = 'bo2',
		sortmethod = 'points'
	},
	bo2nopoints = {
		isbo2 = true,
		columns = 'bo2nopoints',
		sortmethod = 'recordgames'
	},
	bo3 = {
		columns = 'series',
		sortmethod = 'record',
	},
	bo3diff = {
		columns = 'bo3diff',
		sortmethod = 'recordwithgames'
	},
	bo3points = {
		columns = 'seriespt',
		sortmethod = 'recordwithpoints',
	},
	bo3gamepoints = {
		columns = 'bo3gamepoints',
		sortmethod = 'points'
	},
	bo3hiddenpoints = {
		columns = 'series',
		sortmethod = 'recordwithpoints'
	},
	bo3fakepoints = {
		columns = 'series',
		sortmethod = 'recordwithgames'
	},
	bo3orderbygames = {
		columns = 'series',
		sortmethod = 'recordwithgamesnotb'
	}
}

local HEADING_DATA = {
	Place = { name = '', colspan = 1 },
	Games = { colspan = 2, fields = { 'Games', 'GamesPct' } },
	GamesBO1 = { colspan = 2, fields = { 'Series', 'SeriesPct' } },
	GamesDiff = { colspan = 3, fields = { 'Games', 'GamesPct', 'Diff' } },
	Team = { colspan = 1 },
	Series = { colspan = 2, fields = { 'Series', 'SeriesPct' } },
	SeriesBO2 = { colspan = 1 },
	Points = { colspan = 1 },
	PointsTB = { colspan = 1 },
	Group = { colspan = 1 },
	Streak = { colspan = 1 },
}

local FIELD_CLASSES = {
	Place = 'standings-place',
	Team = 'standings-team',
}

local COL_PRELOADS = {
	seriespt = { 'Place', 'Team', 'Series', 'Games', 'PointsTB', 'Streak' },
	series = { 'Place', 'Team', 'Series', 'Games', 'Streak' },
	bo1 = { 'Place', 'Team', 'GamesBO1', 'Streak' },
	bo1points = { 'Place', 'Team', 'Points', 'GamesBO1', 'Streak' },
	bo2 = { 'Place', 'Team', 'Points', 'SeriesBO2', 'Games', 'Streak' },
	bo3diff = { 'Place', 'Team', 'Series', 'GamesDiff', 'Streak' },
	bo2nopoints = { 'Place', 'Team', 'SeriesBO2', 'Games', 'Streak' },
	bo3gamepoints = { 'Place', 'Team', 'Points', 'Games', 'Series', 'Streak' }, -- OPL thing
}

local ARG_ORDER = { 'team', 'w', 't', 'l', 'wg', 'lg', 'p', 'tied', 'Group', 'place', 'footnotes' }

local ARG_ZEROES = { w = true, t = true, l = true, wg = true, lg = true, p = true }

local kv_sep = ':;:'
local arg_sep = ';:;'

--[[
	Structure:
	dataByTeams = {
		team1, team2, team3
		team1 = { data },
		team2 = { data },
	}
]]

local p = {}
local h = {}

function p.fromCargo(frame)
	i18n.initGlobalFromFile('Standings')
	local args = util_args.merge(true)
	local overviewPage = util_esports.getOverviewPage(args.page)
	if util_args.castAsBool(args.storeargs) then
		h.storeArgs(args, overviewPage)
	end
	if util_args.castAsBool(args.argsfromcargo) then
		local exists, message = h.queryArgs(args, overviewPage)
		if util_args.castAsBool(args.forceargquery) and not exists then
			return message
		end
	end
	h.setPreload(args)
	local data = h.getData(overviewPage, args)
	local processed = h.processData(data, args.isbo2, args)
	local fields = h.pickFields(args)
	if h.doWeStoreCargo(args) then
		h.storeCargo(processed, args)
	end
	local includeButton = not util_args.castAsBool(args.nobutton)
	return h.makeOutput(processed, fields, args, includeButton, overviewPage)
end

function p.fromArgs(frame)
	i18n.initGlobalFromFile('Standings')
	local args = util_args.merge(true)
	h.setPreload(args)
	local data = h.getDataFromArgs(args)
	local processed = h.processData(data, args.isbo2, args)
	local fields = h.pickFields(args)
	if h.doWeStoreCargo(args) then
		h.storeCargo(processed, args)
	end
	return h.makeOutput(processed, fields, args, overviewPage)
end

function h.storeArgs(args, page)
	local allArgs = {}
	local rowArgs = {}
	for k, v in pairs(args) do
		allArgs[#allArgs+1] = ('%s%s%s'):format(k, kv_sep, v)
		if k:sub(1,3) == 'row' then
			rowArgs[#rowArgs+1] = ('%s%s%s'):format(k, kv_sep, v)
		end
	end
	local store = {
		_table = 'StandingsArgs',
		OverviewPage = page,
		Args = table.concat(allArgs, arg_sep),
		RowArgs = table.concat(rowArgs, arg_sep),
		Finalorder = args.finalorder,
		TournamentGroup = args.onlygroup,
		N = util_vars.setGlobalIndex('standingsCargoN')
	}
	store.UniqueLine = ('%s_%s'):format(store.OverviewPage,store.N)
	util_cargo.store(store)
end

function h.queryArgs(args, page)
	local query = {
		tables = 'StandingsArgs',
		fields = {'Args','TournamentGroup'},
		where = h.queryArgsWhere(args, page)
	}
	local data = util_cargo.queryAndCast(query)
	if not next(data) then
		return nil, '<div style="min-width:15em">Unable to retrieve standings data</div>'
	elseif data[1].TournamentGroup and not args.onlygroup then
		return nil, '<div style="min-width:17em">Query does not currently support multiple groups</div>'
	elseif #data > 1 then
		return nil, '<div style="min-width:20em">Multiple Results Found - Cannot print single standings table</div>'
	end
	local data_tbl = util_text.split(data[1].Args, arg_sep)
	for _, arg in ipairs(data_tbl) do
		local k, v = arg:match(('(.*)%s(.*)'):format(kv_sep))
		args[k] = args[k] or v
	end
	return true
end

function h.queryArgsWhere(args, page)
	local tbl = {
		('OverviewPage="%s"'):format(page),
		util_cargo.whereFromArg('TournamentGroup="%s"', args.onlygroup)
	}
	return util_table.concat(tbl, ' AND ')
end

function h.setPreload(args)
	if not args.preload then
		return
	end
	local preload = lang:lc(args.preload)
	if not PRELOADS[preload] then
		error('Invalid preload')
	end
	args.isbo2 = util_args.castAsBool(args.isbo2) or PRELOADS[preload].isbo2
	args.columns = args.columns or PRELOADS[preload].columns
	args.sortmethod = args.sortmethod or PRELOADS[preload].sortmethod
end

-- from args
function h.getDataFromArgs(args)
	local i = 1
	local data = h.argsToTable(args)
	h.processArgData(data)
	return data
end

function h.argsToTable(args)
	local tbl = {}
	for i, arg in ipairs(args) do
		local thisdata = util_text.split(arg, argsep)
		local thisteam = m_team.teamlinkname(thisdata[1])
		tbl[i] = thisteam
		tbl[thisteam] = {}
		for k, v in ipairs(thisdata) do
			local key = ARG_ORDER[k]
			if v ~= '' then
				if key == 'footnotes' then
					tbl[thisteam][key] = util_text.split(v,'%s*;;%s*')
				else
					tbl[thisteam][key] = tonumber(v) or v
				end
			elseif ARG_ZEROES[key] then
				tbl[thisteam][key] = 0
			elseif key == 'footnotes' then
				tbl[thisteam][key] = {}
			end
		end
	end
	return tbl
end

function h.processArgData(data)
	local curplace = 1
	for i, team in ipairs(data) do
		local row = data[team]
		if util_args.castAsBool(row.tied) then
			row.sort = curplace
		else
			row.sort = i
			curplace = i
		end
		row.tb = row.p
	end
end

-- from cargo
function h.getData(overviewPage, args)
	local query = h.makeQuery(overviewPage, args)
	local result = util_cargo.queryAndCast(query)
	local teams = h.teamsFromList(args.teamlist)
	local dataByTeams = h.compileTeams(result, teams, util_args.castAsBool(args.onlylist))
	if util_args.castAsBool(args.groups) or args.onlygroup then
		h.addGroupData(dataByTeams, overviewPage, args.onlygroup)
	end
	h.adjustProcessedFromArgs(dataByTeams, args)
	if args.finalorder then
		h.sortTeamsByArg(dataByTeams, args.finalorder, args.finalplaces)
	else
		h.sortTeamsByCargo(dataByTeams, lang:lc(args.sortmethod or 'record'))
	end
	return dataByTeams
end

function h.makeQuery(page, args)
	local query = {
		tables = 'MatchSchedule',
		fields = h.makeFields(args),
		where = h.makeWhere(page, args),
		types = {
			Winner = 'number',
			Team1Points = 'number',
			Team2Points = 'number',
			Team1PointsTB = 'number',
			Team2PointsTB = 'number'
		},
		orderBy = 'N_Page ASC, N_TabInPage ASC, N_MatchInTab ASC',
		limit = 9999,
		groupBy = 'UniqueMatch',
	}
	return query
end

function h.makeFields(args)
	local tbl = {
		'Team1Final=Team1',
		'Team2Final=Team2',
		'Winner',
		'Team1Score',
		'Team2Score',
		'Team1Points',
		'Team2Points',
		'Team1PointsTB',
		'Team2PointsTB',
		'Tab',
	}
	if not util_args.castAsBool(args.nofootnotes) then
		util_table.mergeArrays(tbl, { 'Team1Footnote', 'Team2Footnote' })
	end
	return tbl
end

function h.makeWhere(page, args)
	local tbl = {
		('OverviewPage="%s"'):format(page),
		-- 'Winner IS NOT NULL',
	}
	if lang:lc(args.tiebreakers or '') == 'only' then
		tbl[#tbl+1] = '(IsTiebreaker = "1")'
	elseif not util_args.castAsBool(args.tiebreakers) then
		tbl[#tbl+1] = '(IsTiebreaker != "1" OR IsTiebreaker IS NULL)'
	end
	if args.excludetabs then
		for v in util_text.gsplit(args.excludetabs, sep) do
			tbl[#tbl+1] = ('Tab != "%s"'):format(v)
		end
	end
	if args.onlytabs then
		local onlyrounds_tbl = {}
		for v in util_text.gsplit(args.onlytabs, sep) do
			onlyrounds_tbl[#onlyrounds_tbl+1] = ('Tab = "%s"'):format(v)
		end
		tbl[#tbl+1] = ('(%s)'):format(util_table.concat(onlyrounds_tbl, ' OR '))
	end
	return util_table.concat(tbl, ' AND ')
end	

function h.teamsFromList(teamlist)
	if not teamlist then
		return {}
	end
	local teams = util_args.splitAndMap(teamlist, sep, m_team.teamlinkname)
	for _, team in ipairs(teams) do
		h.addTeamZeros(teams, team)
	end
	return teams
end

function h.compileTeams(result, teams, onlylist)
	local hasteams = next(teams) and true
	local tbl = mw.clone(teams)
	for _, row in ipairs(result) do
		local team1 = m_team.teamlinkname(row.Team1)
		local team2 = m_team.teamlinkname(row.Team2)
		if not hasteams then
			h.initializeTeam(tbl, team1)
			h.initializeTeam(tbl, team2)
			h.addTeamData(tbl[team1], tbl[team2], row)
		elseif onlylist and tbl[team1] and tbl[team2] then
			h.addTeamData(tbl[team1], tbl[team2], row)
		elseif not onlylist then
			if tbl[team1] then
				h.addTeam1Data(tbl[team1], row)
			end
			if tbl[team2] then
				h.addTeam2Data(tbl[team2], row)
			end
		end
	end
	return tbl
end

function h.initializeTeam(tbl, team)
	if team and not tbl[team] then
		tbl[#tbl+1] = team
		h.addTeamZeros(tbl, team)
	end
	return
end

function h.addTeamZeros(tbl, team)
	tbl[team] = {
		w = 0,
		t = 0,
		l = 0,
		p = 0,
		tb = 0,
		wg = 0,
		lg = 0,
		strN = 0,
		footnotes = {},
	}
	return
end

function h.addTeamData(team1, team2, row)
	h.addTeam1Data(team1, row)
	h.addTeam2Data(team2, row)
end

function h.addTeam1Data(team1, row)
	if not row.Winner then return end
	team1.w = team1.w + (row.Winner == 1 and 1 or 0)
	team1.l = team1.l + (row.Winner == 2 and 1 or 0)
	team1.t = team1.t + (row.Winner == 0 and 1 or 0)
	team1.p = team1.p + (row.Team1Points or 0)
	team1.wg = team1.wg + (row.Team1Score or 0)
	team1.lg = team1.lg + (row.Team2Score or 0)
	team1.tb = team1.tb + (row.Team1PointsTB or 0)
	team1.footnotes[#team1.footnotes+1] = h.footnoteText(row.Tab, row.Team1Footnote)
	h.addStreak(team1, row.Winner == 1, row.Winner == 0, row.Winner == 2)
end

function h.addTeam2Data(team2, row)
	if not row.Winner then return end
	team2.w = team2.w + (row.Winner == 2 and 1 or 0)
	team2.l = team2.l + (row.Winner == 1 and 1 or 0)
	team2.t = team2.t + (row.Winner == 0 and 1 or 0)
	team2.p = team2.p + (row.Team2Points or 0)
	team2.wg = team2.wg + (row.Team2Score or 0)
	team2.lg = team2.lg + (row.Team1Score or 0)
	team2.tb = team2.tb + (row.Team2PointsTB or 0)
	team2.footnotes[#team2.footnotes+1] = h.footnoteText(row.Tab, row.Team2Footnote)
	h.addStreak(team2, row.Winner == 2, row.Winner == 0, row.Winner == 1)
end

function h.addStreak(team, isWin, isDraw, isLoss)
	local result = isWin and 'W' or isDraw and 'D' or isLoss and 'L'
	team.strN = result == team.strRes and team.strN + 1 or 1
	team.strRes = result
end

function h.footnoteText(tab, footnote)
	if not footnote then return nil end
	return ('%s: %s'):format(tab, footnote)
end

-- adjust from args
function h.adjustProcessedFromArgs(data, args)
	local arg_adjust = {
		footnotes = h.getAdjustmentArgData(args.footnotes),
		p = h.getAdjustmentArgData(args.pointadjust),
		tb = h.getAdjustmentArgData(args.tbpointadjust)
	}
	for _, teamstr in ipairs(data) do
		local team = data[teamstr]
		team.p = team.p + (tonumber(arg_adjust.p[teamstr] or 0))
		team.tb = team.tb + (tonumber(arg_adjust.tb[teamstr] or 0))
		team.footnotes = h.processFootnotes(team.footnotes, arg_adjust.footnotes[teamstr])
	end 
end

function h.getAdjustmentArgData(arg)
	if not arg then
		return {}
	end
	local tbl = {}
	for val in arg:gmatch('%(%(%((.-)%)%)%)') do
		k, v = val:match('(.*)===(.*)')
		tbl[m_team.teamlinkname(k)] = v
	end
	return tbl
end

function h.processFootnotes(from_team, from_arg)
	from_team[#from_team+1] = from_arg
	local tbl = { Team = from_team }
	return tbl
end

-- sort
function h.sortTeamsByCargo(data, sortmethod)
	local f = util_tournament.getSortMethod(sortmethod)
	util_table.mapDictRowsInPlace(data, f)
	h.sortTeams(data)
end

function h.sortTeamsByArg(data, order, places)
	local order_tbl = util_text.split(order,sep)
	local places_tbl = places and util_text.split(places, sep) or {}
	for k, v in ipairs(order_tbl) do
		local team = data[m_team.teamlinkname(v)]
		if not team then
			error(('%s is not a valid team code'):format(v))
		end
		team.sort = places_tbl[k] and (tonumber(places_tbl[k]) * -1) or (k * -1)
		team.place = places_tbl[k]
	end
	h.sortTeams(data)
end

function h.sortTeams(data)
	table.sort(data,
		function(a,b)
			if data[a].sort == data[b].sort then
				return lang:lc(a) < lang:lc(b)
			else
				return data[a].sort > data[b].sort
			end
		end
	)
end

-- process after sort
function h.addGroupData(data, page, onlygroup)
	local groupdata = h.groupsFromCargo(page)
	for i, team in ipairs(data) do
		if onlygroup and groupdata[team] ~= onlygroup then
			data[i] = false
		else
			data[team].group = groupdata[team]
		end
	end
	util_table.removeFalseEntries(data)
end

function h.groupsFromCargo(page)
	local result = util_tournament.getGroups(page)
	return util_cargo.makeConstDict(result, 'Team', 'GroupName')
end

function h.processData(data, isbo2, args)
	local teamstyle = args.teamstyle or 'rightlonglinked'
	local processed = {}
	local place = 1
	local lastsort
	for k, teamstr in ipairs(data) do
		local team = data[teamstr]
		local thissort = team.sortdisplay or team.sort
		if thissort ~= lastsort then
			place = k
			lastsort = thissort
		end
		processed[#processed+1] = {
			Place = team.place or place,
			Team = m_team[teamstyle](teamstr),
			TeamStr = teamstr,
			Games = ('%s - %s'):format(team.wg, team.lg),
			GamesPct = util_esports.winrate(team.wg, team.lg, 1) .. '%',
			Points = team.p,
			PointsTB = team.tb,
			Group = team.group,
			Diff = util_math.printWithSign(team.wg - team.lg),
			footnotes = team.footnotes,
			Streak = team.strRes and i18n.print('streak' .. team.strRes, team.strN)
		}
		local thisline = processed[#processed]
		if isbo2 then
			thisline.SeriesBO2 = ('%s - %s - %s'):format(team.w, team.t, team.l)
			thisline.SeriesBO2Store = ('%s-%s-%s'):format(team.w, team.t, team.l)
		else
			thisline.Series = ('%s - %s'):format(team.w, team.l)
			thisline.SeriesPct = util_esports.winrate(team.w, team.l, 1) .. '%'
		end
	end
	return processed
end

-- start output stuff
function h.pickFields(args)
	local tbl = {
		headings = h.headingsFromArgs(args),
		fields = {}
	}
	h.addFieldsFromHeadings(tbl.headings, tbl.fields)
	if util_args.castAsBool(args.groups) then
		table.insert(tbl.headings,2,'Group')
		table.insert(tbl.fields,2,'Group')
	end
	return tbl
end

function h.headingsFromArgs(args)
	if args.columnlist then
		return util_text.split(args.columnlist,sep)
	elseif args.columns then
		return COL_PRELOADS[lang:lc(args.columns)]
	else
		return COL_PRELOADS.bo1
	end
end

function h.addFieldsFromHeadings(headings, fields)
	for _, v in ipairs(headings) do
		if HEADING_DATA[v].fields then
			for _, field in ipairs(HEADING_DATA[v].fields) do
				fields[#fields+1] = field
			end
		else
			fields[#fields+1] = v
		end
	end
end

-- print output
function h.makeOutput(processed, fields, args, includeButton, page)
	if util_args.castAsBool(args.forFL) then
		i18n.print = i18n.printForInclusion
	end
	util_footnotes.initializeAllFootnotes()
	local output = mw.html.create('div'):addClass('standings-outer-div')
	local tbl_div = output:tag('div')
	local tbl = tbl_div:tag('table')
		:addClass('wikitable2')
		:addClass('standings')
	h.addFirstHeading(tbl, args, page, #fields.fields)
	h.addHeadings(tbl, fields.headings)
	local classes = h.getClasses(args)
	h.printTable(tbl, processed, fields, classes, includeButton, page)
	util_footnotes.printFootnotes(output)
	return output
end

function h.addFirstHeading(tbl, args, page, colspan)
	local th = tbl:tag('tr'):tag('th'):attr('colspan',colspan)
	args.display = h.getDisplay(args, page)
	if util_args.castAsBool(args.legend) then
		Legend(th, args)
	else
		th:wikitext(args.display or i18n.print('StandingsPlain'))
	end
end

function h.getDisplay(args, page)
	if args.display then
		return args.display
	else
		local eventName = h.getEventName(page)
		if eventName then
			return i18n.print('Standings', eventName)
		end
	end
end

function h.getEventName(page)
	if not page then return nil end
	local query = {
		tables = 'Tournaments',
		fields = 'StandardName',
		where = ('OverviewPage="%s"'):format(page)
	}
	return util_cargo.getOneResult(query)
end

function h.addHeadings(tbl, headings)
	local tr = tbl:tag('tr')
	for _, v in ipairs(headings) do
		tr:tag('th')
			:attr('colspan',HEADING_DATA[v].colspan)
			:wikitext(i18n.print(v))
			:addClass('column-label-small')
	end
end

function h.getClasses(args)
	local classes = {
		places = args.places and util_args.splitAndMap(args.places,sep, h.getClassName) or {}
	}
	local max = #classes.places
	classes.rows = util_args.numberedArgsToTable(args, 'row', true, max) or {}
	util_table.mapInPlace(classes.rows, h.getClassName, max)
	return classes
end

function h.getClassName(class)
	-- prepend every individual "word" in the class with 'standings-'
	return class:gsub('([^ ]+)','standings-%1')
end

function h.printTable(tbl, processed, fields, classes, includeButton, page)
	for i, row in ipairs(processed) do
		local tr = tbl:tag('tr')
			:addClass(classes.rows[i])
		util_esports.addTeamHighlighter(tr, row.TeamStr)
		for _, v in ipairs(fields.fields) do
			local td = tr:tag('td')
				:wikitext(row[v])
				:addClass(FIELD_CLASSES[v])
			if row.footnotes[v] then
				util_footnotes.tagFootnotes(td, row.footnotes[v])
			end
			if v == 'Place' then
				td:addClass(classes.places[i])
			elseif v == 'Team' and includeButton then
				PopupButton.tth(td, page, m_team.short(row.TeamStr) .. ' Schedule', row.TeamStr)
			end
		end
	end
end

function h.doWeStoreCargo(args)
	local ns = mw.title.getCurrentTitle().nsText
	local nocargo = util_args.castAsBool(args.nocargo)
	local isover = util_args.castAsBool(args.isover)
	local useasresults = util_args.castAsBool(args.useasresults)
	local argsfromcargo = util_args.castAsBool(args.argsfromcargo)
	return ns == '' and not nocargo and isover and useasresults and not argsfromcargo
end

function h.storeCargo(data, args)
	local title = mw.title.getCurrentTitle().text
	local N = util_vars.setGlobalIndex('standingsN')
	for i, team in ipairs(data) do
		local tbl = {
			_table = 'TournamentResults',
			Event = util_vars.getVar('Event Name'),
			Phase = util_args.castAsBool(args.groupasphase) and (args.onlygroup or team.Group) or args.phase,
			Tier = util_vars.getVar('Event Tier'),
			Date = util_vars.getVar('Event Date'),
			Place = team.Place,
			['Place_Number'] = team.Place,
			Team = team.TeamStr,
			--TeamLink = team.TeamStr,
			IsAchievement = 'Yes',
			UniqueLine = ('%s_%s_%s'):format(title, N, i),
			LastResult = team.SeriesBO2Store or team.Series,
			GroupName = team.Group,
			-- this should be fixed to not rely on any vardefine, need to add real group support
			LastOpponent_Markup = m_team.rightshort('group stage'),
			-- this should probably display group
			RosterPage = args.rosterpage or title,
		}
		tbl.PageAndTeam = ('%s_%s'):format(tbl.RosterPage, tbl.Team)
		util_cargo.store(tbl)
	end
end

return p