Module:Tennis performance timeline/sandbox explained

local p =

local concat = table.concatlocal insert = table.insertlocal format = mw.ustring.format

local tConfig = mw.loadData("Module:Tennis performance timeline/data")local rounds = tConfig.roundslocal tournaments = tConfig.tournamentslocal environments = tConfig.environmentslocal surfaces = tConfig.surfaceslocal tOrders = tConfig.orders

local curYear = os.date("!*t").yearlocal calendar = mw.loadData("Module:Tennis performance timeline/calendar")

local genders =

local matchTypes =

--Utility functions

local function checkNonNil(value, type) if value

nil then error("Expected " .. type .. ", but is nil", 2) end return valueend

local function checkFormat(str, pattern, type) if str

mw.ustring.match(str, pattern) then return str else error("Invalid " .. type .. ": " .. str, 2) endend

local function checkYear(year) checkNonNil(year, "year") return checkFormat(year, "%d%d%d%d", "year")end

local function checkNum(num, diagMsg) diagMsg = "number" .. (diagMsg and " for " .. diagMsg or "") checkNonNil(num, diagMsg); return checkFormat(num, "%d+", diagMsg)end

local function checkMember(elem, arr, type, diagMsg) if arr[elem] then return elem else diagMsg = type .. (diagMsg and " for " .. diagMsg or "") checkNonNil(elem, diagMsg) local message = insert(message, "Invalid ") insert(message, diagMsg) insert(message, ": ") insert(message, elem) error(concat(message)) endend

-- Format an HTML element with link, tooltip, and colors.local function tooltip(tag, link, tooltip, text, spec) spec = spec or if spec.color then tag:css('color', spec.color) end if spec.italic then tag:wikitext(""); end if spec.bold then tag:wikitext(""); end if link then tag:wikitext("") end if tooltip then if spec.abbr then tag:tag('abbr'):attr('title', tooltip):wikitext(text) else tag:attr('title', tooltip):wikitext(text) end else tag:wikitext(text) end if link then tag:wikitext("") end if spec.bold then tag:wikitext(""); end if spec.italic then tag:wikitext(""); endend

-- Substitute "$[`param`]$" appearing in `str` with `value`.-- For example, subst("$year$ ATP Tour", "year", "1990") -> "1990 ATP Tour"local function subst(str, param, value) if str

nil then return str end return str:gsub("%$" .. param .. "%$", value)end

local function tr return mw.html.create('tr')end

local function th(row) return row:tag('th')end

local function td(row) return row:tag('td')end

local yearslocal datalocal usedRounds

-- parser for data tableslocal function parse(entry, year, tStats) local entryType = type(entry) if entryType

"string" then return entry elseif entryType

"table" then if entry.type

"chrono" then local numericYear = tonumber(year) for _,elem in ipairs(entry) do if type(elem)

"table" and numericYear >= elem[1] then return parse(elem[2], year, tStats) end end return parse(entry.default, year, tStats) elseif entry.type

"switch" then local param = entry.param local arg = param

"year" and year or param

"gender" and data.gender or tStats[param] if entry[arg] then return parse(entry[arg], year, tStats) end return parse(entry.default, year, tStats) else return entry end endend

-- Transform `param` entry in `array` with supplied data.local function transform(array, param, data, tournament, year, country) local entry = array[param] entry = subst(entry, "gender", genders[data.gender]) entry = subst(entry, "matchType", matchTypes[data.matchType]) entry = subst(entry, "year", year) if data[tournament] and data[tournament][year] then local tStats = data[tournament][year] if tournaments[tournament] and tournaments[tournament].region then entry = subst(entry, "region", parse(tournaments[tournament].region[country] or tournaments[tournament].region.default, year, tStats)) end if tournaments[tournament].group then entry = subst(entry, "group", tournaments[tournament].group[tStats.group] or "") end end array[param] = entryend

-- Return wiki page title for the tournament in a given year.local function annualTournamentLink(year, tStats, tInfo) local annualLink = tInfo.annualLink return parse(annualLink, year, tStats)end

--local function header local rows = local row = tr local headerCell = th(row):attr('scope', 'col'):addClass('unsortable') local tooltipSpec = if years.amateur or years.professional then headerCell:attr('rowspan', 2) local eras = if years.amateur then eras[#eras+1] = end if years.professional then eras[#eras+1] = end eras[#eras+1] = eras[#eras+1] = local lastEra = "?" local idx = 1 for _,era in ipairs(eras) do local count = 0 while years[idx] and years[idx] ~= era[1] do count = count + 1 idx = idx + 1 end if count > 0 then th(row):attr('scope', 'col'):attr('colspan', count):wikitext(lastEra) end lastEra = era[2] end tooltip(th(row):attr('scope', 'col'):attr('rowspan', 2), nil, "Strike rate", "SR", tooltipSpec) tooltip(th(row):attr('scope', 'col'):attr('rowspan', 2), nil, "Win–Loss", "W–L", tooltipSpec) th(row):attr('scope', 'col'):attr('rowspan', 2):wikitext("Win %") insert(rows, row) row = tr end if data.categories.count > 1 then headerCell:attr('colspan', 2):wikitext("Tournament") end for _,year in ipairs(years) do local link = subst(parse(tConfig.tours.link, year), "year", year) th(row):attr('scope', 'col'):addClass('unsortable') :wikitext(link and format("%s", link, year) or year) end if #rows

0 then tooltip(th(row):attr('scope', 'col'), nil, "Strike rate", "SR", tooltipSpec) tooltip(th(row):attr('scope', 'col'), nil, "Win–Loss", "W–L", tooltipSpec) th(row):attr('scope', 'col'):wikitext("Win %") end insert(rows, row)

-- Add hatnote if needed. local headerText = if years.last and data.last and tournaments[data.last] then local tInfo = tournaments[data.last] local annualLink = transform(annualLink, "link", data, data.last, years.last) local hatnote = insert(hatnote, "This table includes results through the conclusion of the ") end insert(hatnote, annualName) insert(hatnote, ".") insert(headerText, concat(hatnote)) end insert(headerText, "

" or "", name)) -- Add abbr if doesn't appear in name if abbr and not name:match(abbr) then insert(footnoteChunk, "(" .. abbr .. ")") end if entry[3]

entry[4] then -- One-year event insert(footnoteChunk, "in") insert(footnoteChunk, entry[3]) else if pos > 1 then insert(footnoteChunk, "from") insert(footnoteChunk, entry[3]) end if pos < #footnoteChunks then insert(footnoteChunk, pos

1 and "until" or "to") insert(footnoteChunk, entry[4]) end end insert(footnote, concat(footnoteChunk, (" "))) footnoteChunk = end footnote[#footnote] = "and " .. footnote[#footnote] return frame_:callParserFunction endend

local function winLossStatsSummary(row, stats, boldmarkup, summaryRowspan) boldmarkup = boldmarkup or "" -- − is (nonbreaking) minus sign. local srCell = td(row):attr('data-sort-value', stats.count > 0 and stats.champs / stats.count or -1) :addClass('nowrap') :wikitext(format("%s%d / %d%s", boldmarkup, stats.champs, stats.count, boldmarkup)) local matches = stats.wins + stats.losses local wlCell = td(row):attr('data-sort-value', matches > 0 and stats.wins / matches or -1) local wrCell = td(row) if summaryRowspan then wlCell:attr('rowspan', summaryRowspan) wrCell:attr('rowspan', summaryRowspan) srCell:attr('rowspan', summaryRowspan) end if matches > 0 then wlCell:wikitext(format("%s%d–%d%s", boldmarkup, stats.wins, stats.losses, boldmarkup)) wlCell:addClass("nowrap") wrCell:wikitext(format("%s%.2f%%%s", boldmarkup, stats.wins * 100 / matches, boldmarkup)) else wlCell:wikitext(format("%s–%s", boldmarkup, boldmarkup)) wrCell:attr('data-sort-value', '-1%') :wikitext(format("%s–%s", boldmarkup, boldmarkup)) endend

-- Prepare a Win-Loss row in the performance timeline table.local function winLossStatsRow(row, info, stats, bold, summaryRowspan) local boldmarkup = bold and "" or "" local headerName = info.name and info.name .. " " or "" th(row):attr('scope', 'row') :wikitext(format("%s%sWin–Loss%s", boldmarkup, headerName, boldmarkup)) local span for _,year in ipairs(years) do local yStats = stats[year] local display = if yStats and yStats.wins + yStats.losses > 0 then display.text = format("%s%d–%d%s", boldmarkup, yStats.wins, yStats.losses, boldmarkup) bg_col = 'background-color:#EAECF0' else display.text = format("%s–%s", boldmarkup, boldmarkup) bg_col = 'background-color:#EAECF0' if info.absence then local aConfig = parse(info.absence, year) if aConfig then display.text = aConfig.round display.config = aConfig end end end if span then if span.info.round

display.text then span.span = span.span + 1 else outputSpan(row, span) span = nil end end if not span then if display.config and display.config.span then span = else td(row):wikitext(display.text) :addClass("nowrap") end end end if span then outputSpan(row, span) span = nil end winLossStatsSummary(row, stats, boldmarkup, summaryRowspan)end

-- Return true if the player appears in a given tournament.local function hasTournamentAppearance(tournament) local tournamentType = type(tournament) if tournamentType

"string" then if data[tournament] then return true end elseif tournamentType

"table" then if tournament.type

"chrono" then for _,entry in ipairs(tournament) do if data[entry[2]] then return true end end if data[tournament.default] then return true end else -- TODO other table type end end return falseend

-- Create a fresh statistics table.local function statsFactory return end

-- Generate performance timeline rows for a given tournament level.local function body(level, levelHeaderCell) local entries = local stats = statsFactory local levelInfo = parse(tOrders[level]) local levelInfos = local levelLastAppearance for pos,tournament in ipairs(levelInfo) do if hasTournamentAppearance(tournament) then local tStats = statsFactory local row = tr if #entries

0 and data.categories.count > 1 then levelHeaderCell = th(row):attr('scope', 'row') :css('width', '6.5em') :css('max-width', '10em') end local headerCell = th(row):attr('scope', 'row') local tInfos = local lastAppearance local seenRounds = local span local country = data.country.default for _,year in ipairs(years) do country = data.country[year] or country local yearLevelName = parse(levelInfo.name or levelInfo.tooltip, year) if #entries

0 and (#levelInfos

0 or levelInfos[#levelInfos][1] ~= yearLevelName) then -- Add footnote noting series transition. local startYear = eventFirstYear(levelInfos, levelInfo.name or levelInfo.tooltip, year, yearLevelName) insert(levelInfos,) else levelInfos[#levelInfos][4] = tonumber(year) end local yearTournament = parse(tournament, year) local tInfo = checkNonNil(tournaments[yearTournament], "entry for " .. yearTournament) if #tInfos

0 or tInfos[#tInfos][2] ~= tInfo then -- Add footnote noting tournament transition. local startYear = eventFirstYear(tInfos, tournament, year, yearTournament) insert(tInfos,) else tInfos[#tInfos][4] = tonumber(year) end local display = if data[yearTournament] and data[yearTournament][year] then if not levelLastAppearance or levelLastAppearance < year then levelLastAppearance = year end lastAppearance = tInfo local tyStats = data[yearTournament][year] if not rounds[tyStats.round].nocount then tStats.count = tStats.count + 1 stats.count = stats.count + 1 end if rounds[tyStats.round].strike then tStats.champs = tStats.champs + 1 stats.champs = stats.champs + 1 end if not stats[year] then stats[year] = statsFactory end tStats.wins = tStats.wins + tyStats.wins stats[year].wins = stats[year].wins + tyStats.wins stats.wins = stats.wins + tyStats.wins tStats.losses = tStats.losses + tyStats.losses stats[year].losses = stats[year].losses + tyStats.losses stats.losses = stats.losses + tyStats.losses local annualLink = annualTournamentLink(year, tyStats, tInfo) display.round = tyStats.round display.group = tyStats.group display.link = annualLink transform(display, "link", data, yearTournament, year, country) else display.round = parse(tInfo.absence, year) or "A" end local round = rounds[display.round] if round.absence then local absence = false if year < years.last or not data.last and tonumber(year) < curYear then absence = true elseif year

years.last and data.last and calendar[year] and calendar[year][data.gender] then local tWeek = calendar[year][data.gender].week[yearTournament] local lWeek = calendar[year][data.gender].week[data.last] if tWeek and lWeek and tWeek <= lWeek then absence = true end end if not absence then display.round = nil end end local roundInfo = setmetatable(roundInfo,) transform(roundInfo, "tooltip", data, yearTournament, year) if roundInfo.group then roundInfo.round = display.round .. display.group else roundInfo.round = display.round end display.round = roundInfo.name or roundInfo.round if span then if span.round

display.round then span.span = span.span + 1 else outputSpan(row, span, seenRounds) span = nil end end if not span then if roundInfo.span then span = else local cell = td(row) if roundInfo.round then if roundInfo.bgcolor then cell:css('background', roundInfo.bgcolor) end tooltip(cell, display.link, roundInfo.tooltip, display.round, roundInfo) seenRounds[roundInfo.round] = roundInfo end end end end if span then outputSpan(row, span, seenRounds) span = nil end if lastAppearance then headerCell:wikitext(format("%s%s", lastAppearance.link and lastAppearance.link .. "

" or lastAppearance.abbr and lastAppearance.name .. "" or "", lastAppearance.abbr or lastAppearance.name)) if #tInfos > 1 then local footnote = footnoteText(tInfos, function(entry) local tInfo = entry[2] return tInfo.link, tInfo.name, tInfo.abbr end) headerCell:wikitext(footnote) footnoteCount = footnoteCount + 1 end winLossStatsSummary(row, tStats) insert(entries, row) for _,roundInfo in pairs(seenRounds) do local round = roundInfo.name and not usedRounds[roundInfo.name] and roundInfo.name or roundInfo.round usedRounds[round] = roundInfo end end end end if #entries

0 then return nil end if levelHeaderCell then local levelLink = parse(levelInfo.link, levelLastAppearance) local levelName = parse(levelInfo.name, levelLastAppearance) local levelAbbr = parse(levelInfo.abbr, levelLastAppearance) local levelTooltip = parse(levelInfo.tooltip, levelLastAppearance) local levelString = levelAbbr or levelName if data.categories.count > 1 then levelHeaderCell:attr('rowspan', #entries + 1) end tooltip(levelHeaderCell, levelLink or levelAbbr and levelName, levelTooltip, levelString,) if #levelInfos > 1 then local footnote = footnoteText(levelInfos, function(entry) return entry[2].link, entry[1], entry[2].abbr end) levelHeaderCell:wikitext(footnote) footnoteCount = footnoteCount + 1 end end

local row = tr winLossStatsRow(row,, stats, true) insert(entries, row) local result = for _,entry in ipairs(entries) do insert(result, tostring(entry)) end return concat(result, "\n")end

-- Generate rows for career performance timeline.local function summary(envSummary, headerCell) local entries = local stats = statsFactory local environmentInfo = tOrders.environments local surfaceInfo = tOrders.surfaces local surfaceCount = 0 for _,environment in ipairs(environmentInfo) do if data[environment] then for _,surface in ipairs(surfaceInfo) do if data[environment][surface] then surfaceCount = surfaceCount + 1 end end end end if surfaceCount

0 then return nil end -- Aggregate data. local eStats = local sStats = for _,env in ipairs(environmentInfo) do if data[env] then for _,surface in ipairs(surfaceInfo) do if data[env][surface] then for _,year in ipairs(years) do local esyStats = data[env][surface][year] if esyStats then if not eStats[env] then eStats[env] = statsFactory end if not eStats[env][year] then eStats[env][year] = statsFactory end if not sStats[surface] then sStats[surface] = statsFactory end if not sStats[surface][year] then sStats[surface][year] = statsFactory end if not stats[year] then stats[year] = statsFactory end for key,value in pairs(esyStats) do sStats[surface][year][key] = sStats[surface][year][key] + value sStats[surface][key] = sStats[surface][key] + value eStats[env][year][key] = eStats[env][year][key] + value eStats[env][key] = eStats[env][key] + value stats[year][key] = stats[year][key] + value stats[key] = stats[key] + value end end end end end end end local function venueWinLossStatsRow(venues, vInfo, vStats) for _,venue in ipairs(venues) do if vStats[venue] then local row = tr if #entries

0 and data.categories.count > 1 then -- Add header cell. headerCell = th(row):attr('scope', 'row') end winLossStatsRow(row, vInfo[venue], vStats[venue]) insert(entries, row) end end end venueWinLossStatsRow(surfaceInfo, surfaces, sStats) if envSummary then venueWinLossStatsRow(environmentInfo, environments, eStats) end

local row = tr winLossStatsRow(row,, stats, true, 2) insert(entries, row)

local row = tr local wrHdrCell = th(row):attr('scope', 'row'):wikitext("Win %") for _,year in ipairs(years) do local cellContent = "" if stats[year] then local wins = stats[year].wins local losses = stats[year].losses local matches = wins + losses if matches > 0 then cellContent = format("%.1f%%", wins * 100 / matches) end end td(row):wikitext(cellContent) end insert(entries, row) if headerCell then if data.categories.count > 1 then headerCell:attr('rowspan', #entries) end headerCell:wikitext("Career") end

local function counterStatsRow(row, name, type, bold) local boldmarkup = bold and "" or "" local rowHdrCell = th(row):attr('scope', 'row'):css('text-align', 'right') :wikitext(format("%s%s%s", boldmarkup, name, boldmarkup)) if data.categories.count > 1 then rowHdrCell:attr('colspan', 2) end for _,year in ipairs(years) do td(row):wikitext(format("%s%s%s", boldmarkup, stats[year] and tostring(stats[year][type]) or "–", boldmarkup)) end td(row):attr('colspan', 2) :wikitext(format("%s%d total%s", boldmarkup, stats[type], boldmarkup)) end

local row = tr:addClass('sortbottom') counterStatsRow(row, "Tournaments played", "count") td(row):wikitext(format("%.1f%%", stats.champs * 100 / stats.count)) insert(entries, row)

if stats.finals > 0 then local row = tr:addClass('sortbottom') counterStatsRow(row, "Finals reached", "finals") td(row):attr('rowspan', 2) :wikitext(format("%.1f%%", stats.champs * 100 / stats.finals)) insert(entries, row) local row = tr:addClass('sortbottom') counterStatsRow(row, "Titles", "champs", true) insert(entries, row) end

local row = tr:addClass('sortbottom') local yearEndHdrCell = th(row):attr('scope', 'row'):css('text-align', 'right') :wikitext("Year-end ranking") if data.categories.count > 1 then yearEndHdrCell:attr('colspan', 2) end for _,year in ipairs(years) do local cell = td(row) if data.rank and data.rank[year] then local rank = data.rank[year] local rankConfig = tConfig.rankings[rank] or if rankConfig.bgcolor then cell:css('background', rankConfig.bgcolor) end local boldmarkup = rankConfig.bold and "" or "" cell:wikitext(format("%s%s%s", boldmarkup, rank, boldmarkup)) end end local cell = td(row):attr('colspan', 3) if data.prizemoney then tooltip(cell, nil, "Career prize money", data.prizemoney,) end insert(entries, row)

local result = for _,entry in ipairs(entries) do insert(result, tostring(entry)) end

return concat(result, "\n")end

-- Generate wikitext to conclude performance timeline table, including footnotes.local function footer local result = insert(result, "

-\n
") if footnoteCount > 0 then local reflistTag = mw.html.create("div") reflistTag:addClass("reflist"):css('list-style-type', 'lower-alpha') :wikitext(frame_:callParserFunction) insert(result, tostring(reflistTag)) end return concat(result, "\n")end

-- Return true if the player appears in a given tournament level.local function hasLevelAppearance(level) local levelInfo = parse(tOrders[level]) for _,tournament in ipairs(levelInfo) do if hasTournamentAppearance(tournament) then return true end end return falseend

function p._main(args, frame) frame_ = frame data = years = usedRounds = footnoteCount = 0 data.gender = args.gender or "men" data.matchType = args.matchType or "singles" data.country = data.country.default = args.country or "UNK" local idx = 1 local environmentSummary = true local year while args[idx] do local arg = args[idx] if arg

"year" then idx = idx + 1 year = checkYear(args[idx]) if years.last and year <= years.last then error(format("Nonincreasing year: %s appears after %s", year, years.last)) end years.last = year insert(years, year) elseif arg

"country" then idx = idx + 1 local country = checkNonNil(args[idx], year .. " country") data.country[year] = args[idx] elseif arg

"amateur" then years.amateur = year elseif arg

"professional" then years.professional = year elseif tournaments[arg] then local tournament = arg local diagMsg = year .." " .. tournament local tStats = idx = idx + 1 local round = args[idx] -- Handle zones for Davis Cup. if round and round:sub(1, 2)

"WG" then tStats.round = "WG" tStats.group = round:sub(3) elseif round and round:sub(1, 2)

"PO" then tStats.round = "PO" tStats.group = round:sub(3) elseif round and round:sub(1, 1)

"Z" then tStats.round = "Z" tStats.group = checkNum(round:sub(2), diagMsg .. " zone") else tStats.round = round end checkMember(tStats.round, rounds, "round", diagMsg) idx = idx + 1 tStats.wins = checkNum(args[idx], diagMsg) idx = idx + 1 tStats.losses = checkNum(args[idx], diagMsg) if data[tournament]

nil then data[tournament] = end data[tournament][year] = tStats elseif environments[arg] then local environment = arg local diagMsg = year .. " " .. " " .. environment idx = idx + 1 local surface = checkNonNil(args[idx], diagMsg .. " surface") if surfaces[surface] then diagMsg = diagMsg .. " " .. surface local sStats = idx = idx + 1 sStats.count = checkNum(args[idx], diagMsg) idx = idx + 1 sStats.wins = checkNum(args[idx], diagMsg) idx = idx + 1 sStats.losses = checkNum(args[idx], diagMsg) idx = idx + 1 sStats.champs = checkNum(args[idx], diagMsg) idx = idx + 1 sStats.finals = sStats.champs + checkNum(args[idx], diagMsg) if data[environment]

nil then data[environment] = end if data[environment][surface]

nil then data[environment][surface] = end data[environment][surface][year] = sStats else error(format("Unknown surface (%s %s): %s", year, environment, arg)) end elseif surfaces[arg] then local surface = arg local diagMsg = year .. " " .. surface local sStats = idx = idx + 1 sStats.count = checkNum(args[idx], diagMsg) idx = idx + 1 sStats.wins = checkNum(args[idx], diagMsg) idx = idx + 1 sStats.losses = checkNum(args[idx], diagMsg) idx = idx + 1 sStats.champs = checkNum(args[idx], diagMsg) idx = idx + 1 sStats.finals = sStats.champs + checkNum(args[idx], diagMsg) if data.outdoor

nil then data.outdoor = end if data["outdoor"][surface]

nil then data["outdoor"][surface] = end data["outdoor"][surface][year] = sStats -- Disable summary by environment. environmentSummary = false elseif arg

"rank" then idx = idx + 1 if data.rank

nil then data.rank = end data.rank[year] = checkNum(args[idx], year .. " rank") else error(format("Unknown argument at position %d (%s): %s", idx, year, arg)) end idx = idx + 1 end data.prizemoney = args.prizemoney data.last = args.last data.categories = if args.types then local count = 0 for _,type in ipairs(mw.text.split(args.types, ",")) do if type

"Career" or tConfig.orders[type] and hasLevelAppearance(type) then data.categories[type] = true count = count + 1 end end data.categories.count = count else local count = 0 for _,type in ipairs(parse(tConfig.orders.order)) do if hasLevelAppearance(type) then data.categories[type] = true count = count + 1 end end data.categories.Career = true data.categories.count = count + 1 end local result = local tableHeader, headerRows, headerCell = header insert(result, tableHeader) local function insertHeaderRowsIfNeeded if #result

1 then for _,headerRow in ipairs(headerRows) do insert(result, tostring(headerRow)) end end end for _,level in ipairs(parse(tConfig.orders.order)) do if data.categories[level] then local levelRows = body(level, data.categories.count

1 and headerCell) insertHeaderRowsIfNeeded insert(result, levelRows) end end if data.categories.Career then local careerRows = summary(environmentSummary, data.categories.count

1 and headerCell) insertHeaderRowsIfNeeded if careerRows then insert(result, careerRows) end end insert(result, footer) return concat(result, "\n")end

function p.main(frame) -- Import module function to work with passed arguments local getArgs = require('Module:Arguments').getArgs local args = getArgs(frame) return p._main(args, frame)end

return p