Module:Standings

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' },	bo5 = { columns = 'series', sortmethod = 'record', },	bo5points = { columns = 'seriespt', sortmethod = 'recordwithpoints', }, }

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 }, }

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

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

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) 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, ' Unable to retrieve standings data ' elseif data[1].TournamentGroup and not args.onlygroup then return nil, ' Query does not currently support multiple groups ' elseif #data > 1 then return nil, ' Multiple Results Found - Cannot print single standings table ' 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, footnotes = {} }	return end

function h.addTeamData(team1, team2, row) if not row.Winner then return end h.addTeam1Data(team1, row) h.addTeam2Data(team2, row) end

function h.addTeam1Data(team1, row) 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) end

function h.addTeam2Data(team2, row) 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) 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, }		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 = 'CCMTournaments', 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