Module:User:Mr. Stradivarius/League table

--                                                                                   -- --                                   LEAGUE TABLE                                     -- --                                                                                   -- --  This module generates league table templates for various sports. -- --                                                                                   --

local html_builder = require('Module:HtmlBuilder') local lang = mw.language.getContentLanguage

-- Team class -- -- The team class deals with all the data about a particular team in the league table.

local team = {}

-- Fields whose initial value should be 0. team.stat_fields = { played         = true, wins           = true, draws          = true, losses         = true, goals_for      = true, goals_against  = true, goal_difference = true, points         = true }

-- Fields whose initial value should be nil. team.other_fields = { name    = true, article = true, link    = true, position = true }

team.__index = function (t, k)	if team[k] then return team[k] else -- Make automatic setters and getters for function calls to nonexistent functions. local setget, setgetkey = k:match('^([sg]et)_(.+)$') if setget and setgetkey then if not team.stat_fields[setgetkey] and not team.other_fields[setgetkey] then return -- Not a recognised field, so exit. We don't want people calling team:set_set_points, for example. end if setget == 'set' then return function (t, val) t[setgetkey] = val end elseif setget == 'get' then return function (t) return t[setgetkey] end end end end end

function team.new(display) local obj = {} -- Set display data. obj.display = type(display) == 'table' and display or {} -- Set initial stat values. for field in pairs(team.stat_fields) do		obj[field] = 0 end setmetatable(obj, team) return obj end

function team:set_article(s) self.article = s or self.name end

function team:set_link(s) if s then self.link = s	elseif self.article == self.name then self.link = mw.ustring.format('%s', self.name) else self.link = mw.ustring.format('%s', self.article, self.name) end end

function team:set_played(n) if n then self.played = n	else self.played = self.wins + self.draws + self.losses end end

function team:set_goal_difference(n) if n then self.goal_difference = n	else self.goal_difference = self.goals_for - self.goals_against end end

function team:set_points(n) if n then self.points = n	else self.points = self.wins * 3 + self.draws end end

function team:export local row = html_builder.create('tr') for _, field in ipairs(self.display) do		row .tag('td') .wikitext(self['get_' .. field](self)) end return tostring(row) end

-- league_table class -- -- The leauge_table class defines the behaviour of the league table - how teams are ranked, -- how many entries are displayed, and outputs the final wikicode.

local league_table = {} league_table.__index = league_table

function league_table.new local obj = {} obj.teams = {} -- We need: -- 1. A table of input for each team object. {wins = 2, draws = 5, name = "Arsenal"} etc. -- 2. A table of the data fields to be used in the league table. {wins = true, draws = true, name = true} etc. -- This may be different from the team input as that is typically provided by the end user. -- 3. A table of formatting options. Table style and class, any rows to be coloured, etc.	setmetatable(obj, league_table) return obj end

function league_table:add_team(obj) table.insert(self.teams, obj) end

function league_table:sort -- Sort the table first by points, then by goal difference, then in alphabetical order. table.sort(self.teams, function (a, b)		local a_points = a:get_points		local b_points = b:get_points		if a_points and b_points and a_points > b_points then			return true		elseif a_points and a_points == b_points then			local agd = a:get_goal_difference			local bgd = b:get_goal_difference			if agd and bgd and agd > bgd then				return true			elseif agd and agd == bgd then				local a_name = a:get_name				local b_name = b:get_name				if a_name and b_name then					return a_name < b_name				end			end		end		return false	end) -- If any of the teams specified their position explicitly, use that instead. local positions = {} for i, team in ipairs(self.teams) do		local pos = team:get_position if pos then table.insert(positions, {old_pos = i, new_pos = pos}) end end for _, pos_table in ipairs(positions) do		local obj = table.remove(self.teams, pos_table[old_pos]) table.insert(self.teams, obj, pos_table[new_pos]) end -- Now that we have sorted the table, set the position explicitly for all the teams. -- This makes it easier to deal with the team table after it has been cropped. for i, team_obj in ipairs(self.teams) do		team_obj:set_position(i) end end

function league_table:crop(name, pos, limit, align, more_down) -- This limits the output of the league table to a set of contiguous entries centered around a team or a position. -- name: the name of the team. This is used to find the position if specified. -- pos: the position. Used for the position if no name was specified. -- limit: the number of entries to limit the table output to. Defaults to 3, or 2 if we are at the top or bottom of the table. -- align: whether the specified position appears at the top, bottom or middle of the selection. It is at the center by default. -- more_down: if the selection is center-aligned and an even number of entries are specified, display more down entries than up if this is true. -- This method should be called after the league table has been sorted. local team_count = #self.teams if team_count <= 1 then return end -- If there is only one team, then we don't need to do any calculation. if name then for i, obj in ipairs(self.teams) do			local obj_name = obj:get_name if name == obj_name then pos = i			end end end if not pos then return end if not limit then if pos == 1 or pos == team_count then limit = 2 else limit = 3 end end if limit >= team_count then return end -- If the limit is higher than the team count then we just use the whole table. local start, finish if align == 'top' then -- 'Top' means that the position is at a lower index than all the other entries, as the top team has position 1 in the league table. finish = pos + limit - 1 if finish > team_count then finish = team_count end start = finish - limit + 1 elseif align == 'bottom' then -- 'Bottom' means that the position is at a higher index than all the other entries. start = pos - limit + 1 if start < 1 then start = 1 end finish = start + limit - 1 else -- Assume align == 'center'. local limit_mod2 = limit % 2 local limit_div2 = math.floor(limit / 2) local ideal_start, ideal_finish -- Where the table would start and finish if we weren't limited by the start and end of the table itself. if limit_mod2 == 1 then -- Odd number, more_down doesn't matter. ideal_start = pos - limit_div2 ideal_finish = pos + limit_div2 elseif more_down then -- Even number with more down than up. Down in this case means a higher index, as the top team has position 1 in the league table. ideal_start = pos - limit_div2 + 1 ideal_finish = pos + limit_div2 else -- Even number with more up than down. Up means a lower index number. ideal_start = pos - limit_div2 ideal_finish = pos + limit_div2 - 1 end if ideal_start < 1 then start = 1 finish = start + limit - 1 elseif ideal_finish > team_count then finish = team_count start = finish - limit + 1 else start = ideal_start finish = ideal_finish end end local ret = {} for i = start, finish do		table.insert(ret, self.teams[i]) end self.teams = ret end

function league_table:export local root = html_builder.create('table') root .addClass('wikitable') for _, team_obj in ipairs(self.teams) do		root .wikitext(team_obj:export) end return tostring(root) end

-- Process arguments from #invoke and define the main module structure.

local p = {}

local function getArgNums(prefix, args) -- Returns an array containing the numbers of arguments with a given prefix. local nums = {} for k in pairs(args) do		if type(k) == 'string' then local num = mw.ustring.match(k, '^' .. prefix .. '([1-9][0-9]*)$') num = tonumber(num) if num then table.insert(nums, num) end end end table.sort(nums) return nums end

function p._main(args) local output_table = league_table:new(args) return output_table:export end

function p.main(frame) -- If called via #invoke, use the args passed into the invoking template, or the args passed to #invoke if any exist. -- Otherwise assume args are being passed directly in from the debug console or from another Lua module. local origArgs if frame == mw.getCurrentFrame then origArgs = frame:getParent.args for k, v in pairs(frame.args) do			origArgs = frame.args break end else origArgs = frame end -- Trim whitespace and remove blank arguments. local args = {} for k, v in pairs(origArgs) do		if type(v) == 'string' then v = mw.text.trim(v) end if v ~= '' then args[k] = v		end end return p._main(args) end

-- Testing area

local dts = require('Module:User:Anomie/deepToString').deepToString

function p.test local display = {'played', 'goal_difference', 'points'} local team1 = team.new(display) team1:set_name('Team 1') team1:set_points(6) team1:set_goal_difference(15) local team2 = team.new(display) team2:set_name('Team 2') team2:set_points(20) team2:set_goal_difference(15) local team3 = team.new(display) team3:set_name('Team 3') team3:set_points(2) team3:set_goal_difference(3) local team4 = team.new(display) team4:set_name('Team 4') team4:set_points(20) team4:set_goal_difference(40) local team5 = team.new(display) team5:set_name('Team 5') team5:set_points(15) team5:set_goal_difference(30) local team6 = team.new(display) team6:set_name('Team 6') team6:set_points(8) team6:set_goal_difference(-15) local team7 = team.new(display) team7:set_name('Team 7') team7:set_points(3) team7:set_goal_difference(-15) local team8 = team.new(display) team8:set_name('Team 8') team8:set_points(20) team8:set_goal_difference(40) local mytable = league_table.new mytable:add_team(team1) mytable:add_team(team2) mytable:add_team(team3) mytable:add_team(team4) mytable:add_team(team5) mytable:add_team(team6) mytable:add_team(team7) mytable:add_team(team8) mytable:sort mytable:crop('Team 6', nil, 4, nil, false) return mytable:export end

return p