Module:Sandbox/Julio974fr

From WikiProjectMed
Jump to navigation Jump to search
require('strict')
local p = {};
local political_party = require('Module:Political party')

local LANG = 'en'
local FALLBACK_LANGS = {'de', 'fr', 'it', 'rm'}

--[[ Properties shorthands ]]
local P_COUNTRY              = 'P17'
local P_MEMBER_OF_PARTY      = 'P102'
local P_FOLLOWS              = 'P155'
local P_FOLLOWED_BY          = 'P156'
local P_HAS_PARTS            = 'P527'
local P_PART_OF              = 'P361'
local P_COLOR                = 'P465'
local P_APPLIES_TO_PART      = 'P518'
local P_POINT_IN_TIME        = 'P585'
local P_CANDIDATE            = 'P726'
local P_SUCCESSFUL_CANDIDATE = 'P991'
local P_VOTES_RECEIVED       = 'P1111'
local P_REPLACED_BY          = 'P1366'
local P_SEATS                = 'P1410'
local P_TOTAL_VALID_VOTES    = 'P1697'
local P_SHORT_NAME           = 'P1813'
local P_ELIGIBLE_VOTERS      = 'P1867'
local P_BALLOTS_CAST         = 'P1868'
local P_NAME                 = 'P2561'
local P_OBJECT_HAS_ROLE      = 'P3831'
local P_CANDIDATE_NUMBER     = '4243'
local P_SPOILT_VOTES         = 'P5044'
local P_BLANK_VOTES          = 'P5045'

--[[ Get a best ranking statement value from "item" with the property "property".
	 The value is returned through the function "typefunc". ]] -- Function copied from elsewhere
local function getStatementValue(item, property, typefunc)
	local statements = mw.wikibase.getBestStatements(item, property)
	if statements[1] and statements[1].mainsnak.snaktype == 'value' then
		return typefunc(statements[1].mainsnak.datavalue.value)
	end
end

local function getStatementQualifier(item, property, qualifier, typefunc)
	local statements = mw.wikibase.getBestStatements(item, property)
	if statements[1] and statements[1].qualifiers and statements[1].qualifiers[qualifier] and statements[1].qualifiers[qualifier][1] then
		return typefunc(statements[1].qualifiers[qualifier][1].datavalue.value)
	end
end

--[[ The "typefunc"s - A series of functions that extract the wanted information
     from a statement value depending on the value type. ]]
local function getAmount(value)		return tonumber(value.amount) end
local function getString(value)		return value end
local function getItem(value)		return value.id end
local function getTimestamp(value)	return value.time end

-- Finds the text value of a given property in the given language
local function getValueFromLanguage(qid, property, lang)
	local statement = mw.wikibase.getBestStatements(qid, property)
	
	for i, v in ipairs(statement) do
		if v.mainsnak.datavalue.value.language == lang then
			return v.mainsnak.datavalue.value.text
		end
	end
	
	for i, v in ipairs(statement) do
		if v.mainsnak.datavalue.value.language == 'mul' then
			return v.mainsnak.datavalue.value.text
		end
	end
end



local function getPartyColor(qid, articleTitle)
	
	local color = getStatementValue(qid, P_COLOR, getString)
	if color then
		return '#'..color
	end
	
	if articleTitle == nil then
		return '#ffffff'
	end
	
	color = political_party._fetch({articleTitle, 'color', error='#ffffff'})
	color = mw.ustring.gsub(color, '&(#)35;', '%1')
	return color
end

local function sortParties(listParties)
	local function comparePartyByVotes(partyA, partyB)
		if partyB.qid == 'Q86630688' then -- Q86630688 is 'Others'
			return true
		elseif partyA.qid == 'Q86630688' then
			return false
		elseif (partyA.votes1 or 0) > (partyB.votes1 or 0) then -- compare votes
			return true
		elseif (partyA.votes1 or 0) < (partyB.votes1 or 0) then
			return false
		elseif (partyA.seats or 0) > (partyB.seats or 0) then -- compare seats
			return true
		elseif (partyA.seats or 0) < (partyB.seats or 0) then
			return false
		elseif (partyA.number or 0) < (partyB.number or 0) then -- compare electoral numbers (comparisons reversed, smaller numbers first)
			return true
		elseif (partyA.number or 0) > (partyB.number or 0) then
			return false
		elseif (partyA.candidateName or partyA.partyArticle or partyA.partyName or '') < (partyB.candidateName or partyB.partyArticle or partyB.partyName or '') then -- compare candidate names (comparisons reversed, first letters first)
			return true
		elseif (partyA.candidateName or partyA.partyArticle or partyA.partyName or '') > (partyB.candidateName or partyB.partyArticle or partyB.partyName or '') then
			return false
		else
			return ((partyA.seats or 0) > (partyB.seats or 0))
		end
	end
	table.sort(listParties, comparePartyByVotes)
end



local function formatWikilink_simple(link, displaytext)
	return '[['..link..'|'..displaytext..']]'
end

local function getAndFormatInterlanguageLinks(qid)
	local fallbackLinks = {}
	for _, language in ipairs(FALLBACK_LANGS) do
		local sitelink = mw.wikibase.getSitelink(qid, language..'wiki')
		if sitelink then
			fallbackLinks[#fallbackLinks+1] = formatWikilink_simple(':'..language..':'..sitelink, language)
		end
	end
	
	if #fallbackLinks == 0 then return '' end
	
	return ' <span style="font-size:85%">[&ZeroWidthSpace;'..table.concat(fallbackLinks, '; ')..']</span>'
end

local function formatWikilink(link, displaytext, qid)
	if link then
		if displaytext then
			return '[['..link..'|'..displaytext..']]'
		else
			return '[['..link..']]'
		end
	else
		if displaytext then
			return displaytext..getAndFormatInterlanguageLinks(qid)
		else
			return '[[wikidata:'..qid..']]'..getAndFormatInterlanguageLinks(qid)
		end
	end
end



local function fetchCandidacyData_party(partyQid)
	local data = {}
	data.candidacyType = 'party'
	
	-- Get party qid
	data.partyQid = partyQid
	
	-- Get party article
	local partyArticle, label
	partyArticle = mw.wikibase.getSitelink(partyQid)
	if not partyArticle then
		label = mw.wikibase.getLabel(partyQid)
	end
	data.partyArticle = partyArticle
	
	-- Get party name
	data.partyName = getValueFromLanguage(partyQid, P_NAME, LANG) or label
	
	return data
end

local function fetchCandidacyData_partyShort(partyQid)
	local data = {}
	
	-- Get party
	data.partyQid = partyQid
	
	if not data.partyQid then
		return data
	end
	
	data.partyArticle = mw.wikibase.getSitelink(data.partyQid)
	data.partyName = getValueFromLanguage(data.partyQid, P_SHORT_NAME, LANG)
	
	return data
end


local function fetchCandidacyData_candidate(candidateQid)
	local data = {}
	data.candidacyType = 'candidate'
	
	data.candidateQid = candidateQid
	
	-- Get article name
	local candidateArticle, label
	data.candidateArticle = mw.wikibase.getSitelink(candidateQid)
	data.candidateName = mw.wikibase.getLabel(candidateQid)
	
	return data
end

-- For a given candidacy: gets their name, article link, party shortlink, and color
local function fetchCandidacyData(qid, candidacyType)
	if candidacyType == 'party' then
		return fetchCandidacyData_party(qid)
	elseif candidacyType == 'candidate' then
		return fetchCandidacyData_candidate(qid)
	else
		error('candidacyType not specified')
	end
end

-- Returns the candidacy qid from a candidacy statement
local function getCandidacyQidFromStatement(candidacyStatement, candidacyType, doGroupByParty)
	if doGroupByParty then
		if candidacyStatement.qualifiers[P_MEMBER_OF_PARTY] then
			return candidacyStatement.qualifiers[P_MEMBER_OF_PARTY][1].datavalue.value.id
		end
	end
	
	return candidacyStatement.mainsnak.datavalue.value.id
end

-- function internal to findCandidacyIndex
local function findCandidacyIndex_finder(listParties, partyQid)
	local finalPartyIndex = nil
	for curPartyindex, curParty in ipairs(listParties) do
		if curParty.qid == partyQid then
			finalPartyIndex = curPartyindex
			break
		end
	end
	return finalPartyIndex
end

-- Returns the index of a candidacy in a list given its qid. If it is not present and doCreate is true, creates an index for it
local function findCandidacyIndex(listCandidacies, candidacyQid, candidacyType, doCreate)
	local candidacyIndex = findCandidacyIndex_finder(listCandidacies, candidacyQid)
	
	if not doCreate then
		return candidacyIndex
	end
	
	if not candidacyIndex then
		if candidacyType == 'party' then
			table.insert(listCandidacies, {qid=candidacyQid, votes1 = 0, seats = 0})
		elseif candidacyType == 'candidate' then
			table.insert(listCandidacies, {qid=candidacyQid, votes1 = 0})
		else
			error('candidacyType not specified')
		end
		candidacyIndex = #listCandidacies
	end
	
	return candidacyIndex
end

-- Returns a table with data from the candidacy's statement
local function addCandidacyDataFromStatement(candidacyData, statement, candidacyType, round, doPrevious, isWinning)
	local roundKey
	if round then roundKey = tostring(round)
	else roundKey = '1' end
	
	if doPrevious then
		if not candidacyData.prevVotes then candidacyData.prevVotes = 0 end
		if statement.qualifiers[P_VOTES_RECEIVED] then
			candidacyData.prevVotes = candidacyData.prevVotes + getAmount(statement.qualifiers[P_VOTES_RECEIVED][1].datavalue.value)
		end
		
		if not candidacyData.prevSeats then candidacyData.prevSeats = 0 end
		if statement.qualifiers[P_SEATS] then
			candidacyData.prevSeats = candidacyData.prevSeats + getAmount(statement.qualifiers[P_SEATS][1].datavalue.value)
		end
		
		return candidacyData
	end
	
	if isWinning then
		candidacyData.winning = round
	end
	
	if not statement.qualifiers then
		return candidacyData
	end
	
	if statement.qualifiers[P_VOTES_RECEIVED] then
		if not candidacyData['votes'..roundKey] then candidacyData['votes'..roundKey] = 0 end
		candidacyData['votes'..roundKey] = candidacyData['votes'..roundKey] + getAmount(statement.qualifiers[P_VOTES_RECEIVED][1].datavalue.value)
	end
	
	if statement.qualifiers[P_SEATS] then
		if not candidacyData.seats then candidacyData.seats = 0 end
		candidacyData.seats = candidacyData.seats + getAmount(statement.qualifiers[P_SEATS][1].datavalue.value)
	end
	
	if statement.qualifiers[P_MEMBER_OF_PARTY] then
		candidacyData.partyQid = getItem(statement.qualifiers[P_MEMBER_OF_PARTY][1].datavalue.value)
	end
	
	if statement.qualifiers[P_OBJECT_HAS_ROLE] and getItem(statement.qualifiers[P_OBJECT_HAS_ROLE][1].datavalue.value) == 'Q42841' then -- Q42841 means incumbent
		candidacyData.isIncumbent = true
	end
	
	if statement.qualifiers[P_FOLLOWS] then
		if not candidacyData.predecessors then candidacyData.predecessors = {} end
		for _, predecessorStatement in ipairs(statement.qualifiers[P_FOLLOWS]) do
			table.insert(candidacyData.predecessors, getItem(predecessorStatement.datavalue.value))
		end
	end
	
	return candidacyData
end

local function findPartyPredecessorOverride(listParties, partyQid, partyIndex)
	for tempIndex, tempParty in ipairs(listParties) do
		if tempParty.predecessors then
			for _, tempPredecessor in ipairs(tempParty.predecessors) do
				if tempPredecessor == partyQid then
					return tempIndex
				end
			end
		end
	end
	
	return partyIndex
end

-- function internal to insertCandidacyInList
local function insertCandidacyInList_previous(listCandidacies, candidacyStatement, candidacyType, candidacyQid)
	local candidacyIndex = findCandidacyIndex(listCandidacies, candidacyQid, candidacyType, false)
	
	candidacyIndex = findPartyPredecessorOverride(listCandidacies, candidacyQid, candidacyIndex)
	
	if not candidacyIndex then
		candidacyIndex = findCandidacyIndex(listCandidacies, getStatementValue(candidacyQid, P_REPLACED_BY, getItem), candidacyType, false)
	end
	
	if candidacyIndex then
		listCandidacies[candidacyIndex] = addCandidacyDataFromStatement(listCandidacies[candidacyIndex], candidacyStatement, candidacyType, 1, true)
	end
	
	return listCandidacies
end

-- Inserts a candidacy in the list of candidacies
local function insertCandidacyInList(listCandidacies, candidacyStatement, candidacyType, round, doPrevious, doGroupByParty, isWinning)
	local candidacyQid = getCandidacyQidFromStatement(candidacyStatement, candidacyType, doGroupByParty)
	
	if doPrevious then
		return insertCandidacyInList_previous(listCandidacies, candidacyStatement, candidacyType, candidacyQid)
	end
	
	local candidacyIndex = findCandidacyIndex(listCandidacies, candidacyQid, candidacyType, true)
	
	listCandidacies[candidacyIndex] = addCandidacyDataFromStatement(listCandidacies[candidacyIndex], candidacyStatement, candidacyType, round, false, isWinning)
	
	return listCandidacies
end

local function addPartyPredecessorsByComponents(listParties)
	for partyIndex, partyData in ipairs(listParties) do
		local allParts = mw.wikibase.getAllStatements(partyData.qid, P_HAS_PARTS)
		for _, predecessorStatement in ipairs(allParts) do
			if not partyData.predecessors then partyData.predecessors = {} end
			table.insert(partyData.predecessors, getItem(predecessorStatement.mainsnak.datavalue.value))
		end
		listParties[partyIndex] = partyData
	end
	return listParties
end

-- Retrieves the list of parties and sorts it by votes/seats
local function getCandidaciesDataFromElection(roundsQidsList, candidacyType, previousQid, doGroupByParty)
	local listCandidacies = {}
	
	-- For each round
	for round, roundQid in ipairs(roundsQidsList) do
		
		-- For each candidate
		local allStatementsInElection = mw.wikibase.getAllStatements(roundQid, P_CANDIDATE)
		for _, candidacyStatement in ipairs(allStatementsInElection) do
			insertCandidacyInList(listCandidacies, candidacyStatement, candidacyType, round, false, doGroupByParty)
		end
		
		local allWinningStatementsInElection = mw.wikibase.getAllStatements(roundQid, P_SUCCESSFUL_CANDIDATE)
		for _, winningStatement in ipairs(allWinningStatementsInElection) do
			insertCandidacyInList(listCandidacies, winningStatement, candidacyType, round, false, false, true)
		end
	end
	
	-- For each party in the previous election
	if previousQid then
		listCandidacies = addPartyPredecessorsByComponents(listCandidacies)
		
		local allStatementsPrevElection = mw.wikibase.getAllStatements(previousQid, P_CANDIDATE)
		for _, partyStatement in ipairs(allStatementsPrevElection) do
			insertCandidacyInList(listCandidacies, partyStatement, 'party', 1, true, doGroupByParty)
		end
	end
	
	return listCandidacies
end

local function getAllCandidaciesData(roundsQidsList, candidacyType, previousQid, partyNameOverrides, doGroupByParty)
	
	local candidaciesData = getCandidaciesDataFromElection(roundsQidsList, candidacyType, previousQid, doGroupByParty)
	
	for candidacyIndex, candidacyData in ipairs(candidaciesData) do
		local fetchedCandidacyData = fetchCandidacyData(candidacyData.qid, candidacyType)
		
		if candidacyType == 'candidate' then
			local fetchedPartyData = fetchCandidacyData_partyShort(candidacyData.partyQid)
			candidacyData.partyName = candidacyData.partyName or fetchedPartyData.partyName
			candidacyData.partyArticle = candidacyData.partyArticle or fetchedPartyData.partyArticle
		end
		
		candidacyData.partyQid = candidacyData.partyQid or fetchedCandidacyData.partyQid
		candidacyData.partyName = candidacyData.partyName or fetchedCandidacyData.partyName
		candidacyData.partyArticle = candidacyData.partyArticle or fetchedCandidacyData.partyArticle
		candidacyData.candidateQid = candidacyData.candidateQid or fetchedCandidacyData.candidateQid
		candidacyData.candidateName = candidacyData.candidateName or fetchedCandidacyData.candidateName
		candidacyData.candidateArticle = candidacyData.candidateArticle or fetchedCandidacyData.candidateArticle
		
		if candidacyData.partyQid then
			candidacyData.partyWikilink = formatWikilink(candidacyData.partyArticle, candidacyData.partyName, candidacyData.partyQid)
		end
		if candidacyData.candidateQid then
			candidacyData.candidateWikilink = formatWikilink(candidacyData.candidateArticle, candidacyData.candidateName, candidacyData.candidateQid)
		end
		if candidacyData.partyQid then
			candidacyData.color = getPartyColor(candidacyData.partyQid, candidacyData.partyArticle)
		end
		
		if candidacyData.partyQid and partyNameOverrides['name_override_'..(candidacyData.partyQid)] then -- If name override specified
			candidacyData.name = partyNameOverrides['name_override_'..(fetchedCandidacyData.partyQid)]
		end
		
		candidaciesData[candidacyIndex] = candidacyData
	end
	
	sortParties(candidaciesData)
	
	return candidaciesData
end

local function getElectionSitelink(qid)
	
	local articleTitle = mw.wikibase.getSitelink(qid)
	
	local supersetElectionQid = getStatementValue(qid, P_PART_OF, getItem)
	while (not articleTitle) or supersetElectionQid do
		qid = supersetElectionQid
		supersetElectionQid = getStatementValue(qid, P_PART_OF, getItem)
		articleTitle = mw.wikibase.getSitelink(qid)
	end
	
	return articleTitle
end

local function filterArgs(args, filter)
	local matched = {}
	for k, v in pairs(args) do
		if string.match(k, filter) then
			matched[k] = v
		end
	end
	return matched
end

local function getYear(timestamp)
	return tonumber(string.match(timestamp, '%+(%d%d%d%d)%-%d%d%-%d%dT%d%d:%d%d:%d%d'))
end

local function getReferences(electionQid)
	local allReferences = {}
	
	local function getReferenceIndex(allReferences, reference)
		local url = reference.snaks.P854[1].datavalue.value
		for i, curRef in ipairs(allReferences) do
			if curRef.url == url then
				return i
			end
		end
		table.insert(allReferences, {url=url})
		return #allReferences
	end
	
	local function addReference(allReferences, reference)
		local refIndex = getReferenceIndex(allReferences, reference)
		-- TODO: Add other reference attributes
		-- allReferences[refIndex].url = uwu
	end

	for _, property in ipairs({P_CANDIDATE, P_ELIGIBLE_VOTERS, P_BALLOTS_CAST, P_SPOILT_VOTES, P_TOTAL_VALID_VOTES, P_BLANK_VOTES}) do
		for _, statement in ipairs(mw.wikibase.getBestStatements(electionQid, property)) do
			if statement.references then
				for _, reference in ipairs(statement.references) do
					addReference(allReferences, reference)
				end
			end
		end
	end
	return allReferences
end

local function round(n)
	local quotient, remainder = math.modf(n)
	if remainder >= 0.5 then return quotient+1
	else return quotient end
end

local function getElectionStatistics(qid, fallbackQid)
	
	local statistics
	if fallbackQid then -- fallbackQid is used to handle the fact data can be in the main item or in a separate "first round" item
		statistics = getElectionStatistics(fallbackQid)
	else
		statistics = {}
	end
	
	if not qid then return {} end
	
	statistics.eligibleVoters = getStatementValue(qid, P_ELIGIBLE_VOTERS, getAmount) or statistics.eligibleVoters
	statistics.totalBallots = getStatementValue(qid, P_BALLOTS_CAST, getAmount) or statistics.totalBallots
	statistics.invalidBallots = getStatementQualifier(qid, P_BALLOTS_CAST, P_SPOILT_VOTES, getAmount) or statistics.invalidBallots
	statistics.blankBallots = getStatementQualifier(qid, P_BALLOTS_CAST, P_BLANK_VOTES, getAmount) or statistics.blankBallots
	
	statistics.invalidVotes = getStatementValue(qid, P_SPOILT_VOTES, getAmount) or statistics.invalidVotes
	statistics.blankVotes = getStatementValue(qid, P_BLANK_VOTES, getAmount) or statistics.blankVotes
	statistics.validVotes = getStatementValue(qid, P_TOTAL_VALID_VOTES, getAmount) or statistics.validVotes
	
	statistics.validBallots = (statistics.totalBallots or 0) - (statistics.invalidBallots or 0) - (statistics.blankBallots or 0)
	statistics.totalVotes = (statistics.validVotes or 0) + (statistics.invalidVotes or 0) + (statistics.blankVotes or 0)
	
	statistics.magnitude = round(statistics.totalVotes / statistics.validBallots)
	
	return statistics
end

local function getRounds(electionQid)
	local rounds = {}
	
	for _,partStatement in ipairs(mw.wikibase.getAllStatements(electionQid, P_HAS_PARTS)) do
		local roundQid = partStatement.mainsnak.datavalue.value.id
		local roundNumber = partStatement.qualifiers.P1545[1].datavalue.value
		
		rounds[tonumber(roundNumber)] = roundQid
	end
	
	if rounds == {} then return {electionQid} end
	
	return rounds
end

local function getSeatsTotals(partiesData)
	local totalSeats = 0
	local totalSeatsChange = 0
	for _, partyData in ipairs(partiesData) do
		totalSeats = totalSeats + (partyData.seats or 0)
		totalSeatsChange = totalSeatsChange + (partyData.seats or 0) - (partyData.prevSeats or 0)
	end
	return totalSeats, totalSeatsChange
end





-- Formatting functions (some copied from Module:Election_results)
local lang = mw.getContentLanguage()
local function fmt(n)
	return n and tonumber(n) and lang:formatNum(tonumber(n)) or nil
end

local function pct(n, d)
	n, d = tonumber(n), tonumber(d)
	if n and d and d > 0 then
		return string.format('%.2f', n / d * 100)
	end
	return '&ndash;'
end

local function formatMultiplier(n, d)
	local n, d = tonumber(n), tonumber(d)
	if (not n) or (not d) or d == 0 then return '&ndash;' end
	
	local num = tostring(round(n/d))
	return tostring(num)..'×'
end

local function diff(n, prevN)
	if not n then
		return '&ndash;'
	elseif not prevN then
		return "''New''"
	end
	
	n, prevN = tonumber(n), tonumber(prevN)
	
	if n > prevN then
		return '+'..tostring(n-prevN)
	elseif prevN > n then
		return '−'..tostring(prevN-n)
	else
		return '±0'
	end
end

local function diffPct(n, d, prevN, prevD)
	if not n or not d or not prevD then
		return '&ndash;'
	elseif not prevN then
		return "''New''"
	end
	
	n = tonumber(n) / tonumber(d)
	prevN = tonumber(prevN) / tonumber(prevD)
	
	if n > prevN then
		return '+'..string.format('%.2f', (n - prevN) * 100)
	elseif prevN > n then
		return '−'..string.format('%.2f', (prevN - n) * 100)
	else
		return '±0.00'
	end
end

local function diffKey(n, prevN)
	if (not prevN) then return n end
	return n - prevN
end

local function diffKeyPct(n, d, prevN, prevD)
	if not n or not d or not prevD then return nil end
	
	n = tonumber(n) / tonumber(d)
	
	if not prevN then return n end
	
	prevN = tonumber(prevN) / tonumber(prevD)
	
	return n - prevN
end





--[[ Table-generating functions ]]

local function beginTable(classes, electionQid)
	
	local root = mw.html.create('span')
	root:attr('id', electionQid..'_resultsTable')
	
	local tab = root:tag('table')
	
	tab:addClass(classes)
	
	local tableCaption = tab:tag('caption')
	
	local caption = 'Results of the ' .. mw.wikibase.getLabel(electionQid)
	
	local previousElectionQid = getStatementValue(electionQid, P_FOLLOWS, getItem)
	local nextElectionQid = getStatementValue(electionQid, P_FOLLOWED_BY, getItem)
	
	if previousElectionQid then
		caption = '[['..getElectionSitelink(previousElectionQid)..'#'..previousElectionQid..'_resultsTable|←]] ' .. caption
	end
	if nextElectionQid then
		caption = caption .. ' [['.. getElectionSitelink(nextElectionQid)..'#'..nextElectionQid..'_resultsTable|→]]'
	end
	
	tableCaption:wikitext(caption)
	tableCaption:done()
	
	tab:done()
	return root, tab
end

local function addHeaderCell(row, wikitext, colspan, rowspan, dataSortType)
	local cell = row:tag('th')
	cell:wikitext(wikitext)
	cell:attr('scope', 'col')
	if colspan then cell:attr('colspan', tostring(colspan)) end
	if rowspan then cell:attr('rowspan', tostring(rowspan)) end
	if dataSortType then cell:attr('data-sort-type', dataSortType) end
	cell:done()
end

local function addCell(row, wikitext, align, colspan, isBold, sortValue)
	local cell = row:tag('td')
	if align then cell:css('text-align', align) end
	if colspan then cell:attr('colspan', tostring(colspan)) end
	if isBold then cell:css('font-weight', 'bold') end
	if sortValue then cell:attr('data-sort-value', sortValue) end
	cell:wikitext(wikitext)
	cell:done()
end

local function addEmptyCell(row, colspan)
	local cell = row:tag('td')
	if colspan then cell:attr('colspan', tostring(colspan)) end
	cell:css('border', 'none')
	cell:done()
end

local function addColorCell(row, color)
	local cell = row:tag('td')
	cell:css('width', '0px')
	cell:css('background-color', color)
	cell:done()
end

local function fillDiagramCell(diagramCell, wikitext, colspan)
	diagramCell:wikitext(wikitext)
	diagramCell:css('text-align', 'center')
	diagramCell:css('background', '#F8F9FA')
	diagramCell:attr('colspan', colspan)
	diagramCell:done()
end

local function addStatRow(root, rowTitle, titleColspan, statisticsTables, statistic, fractionNumerator, fractionDenominator, doMakeBold, doUseMultiplier)
	local isStatisticPresent
	for _, roundStatistics in ipairs(statisticsTables) do -- Check if the statistic is even present
		if roundStatistics[statistic] then
			isStatisticPresent = true
			break
		end
	end
	
	local row
	if isStatisticPresent then
		row = root:tag('tr')
		row:addClass('sortbottom')
		
		if doMakeBold then
			row:css('font-weight', 'bold')
		end
		
		addCell(row, rowTitle, 'left', titleColspan)
		for _,roundStatistics in ipairs(statisticsTables) do
			if roundStatistics[statistic] then
				addCell(row, fmt(roundStatistics[statistic]), 'right')
				if doUseMultiplier then
					addCell(row, formatMultiplier(roundStatistics[fractionNumerator], roundStatistics[fractionDenominator] or 1), 'right')
				else
					addCell(row, pct(roundStatistics[fractionNumerator], roundStatistics[fractionDenominator]), 'right')
				end
			end
		end
	end
	return row
end

local function addReference(refsCell, ref)
	local r = refsCell:wikitext(
		mw.getCurrentFrame():extensionTag({
			name = 'ref',
			content = ref.url
		})
	)
	return r
end

local function formatReferencesRow(root, references, electionQid, cols, hasIncumbent)
	-- References and wikidata link row
	local row = root:tag('tr')
	row:addClass('sortbottom')
	row:css('font-size', '90%')
	local refsCell = row:tag('td')
	refsCell:wikitext('[[:wikidata:'..electionQid..'|See on Wikidata]] – [[Module:Sandbox/Julio974fr|See template]]') -- @ todo update link when publishing
	refsCell:attr('colspan', cols)
	if #references > 0 then
		refsCell:wikitext(' – Sources')
		for _, ref in ipairs(references) do
			addReference(refsCell, ref)
		end
	end
	--[[if hasIncumbent then -- Removed to replace with a {{sup|{{abbr|inc.|Incumbent}}}}, but not sure it will stay
		refsCell:wikitext(' – * incumbent')
	end]]
end

local function checkElectionProperty(candidaciesData, property)
	for _,candidacy in ipairs(candidaciesData) do
		if candidacy[property] and candidacy[property] ~= 0 then return true end
	end
	return false
end

local function getElectionProperties(candidaciesData)
	local properties = {}
	
	if checkElectionProperty(candidaciesData, 'partyQid') then properties.party = true end
	if checkElectionProperty(candidaciesData, 'seats') then properties.seats = true end
	if checkElectionProperty(candidaciesData, 'votes1') then properties.round1 = true end
	if checkElectionProperty(candidaciesData, 'votes2') then properties.round2 = true end
	if checkElectionProperty(candidaciesData, 'prevVotes') or checkElectionProperty(candidaciesData, 'prevSeats') then properties.previousElection = true end
	
	return properties
end

local function getSumOfVotes(candidaciesData, round)
	local total = 0
	local hasVotes = false
	for _,candidacy in ipairs(candidaciesData) do
		if candidacy['votes'..round] then hasVotes = true end
		total = total + (candidacy['votes'..round] or 0)
	end
	if hasVotes then return total end
end





--[[ Main functions ]]

function p._ch_proportional(args)
	
	local electionQid = args.qid or args.election or args[1]
	local previousElectionQid = getStatementValue(electionQid, P_FOLLOWS, getItem)
	
	local doGroupByParty = args.groupByParty or false
	
	local cols = 0
	local year = getYear(getStatementValue(electionQid, P_POINT_IN_TIME, getTimestamp))
	
	local totalVotes = getStatementValue(electionQid, P_TOTAL_VALID_VOTES, getAmount)
	
	local statistics = getElectionStatistics(electionQid)
	statistics.validVotes = statistics.validVotes or totalVotes
	mw.logObject({statistics}, 'statistics')
	
	local prevTotalVotes
	if previousElectionQid then prevTotalVotes = getStatementValue(previousElectionQid, P_TOTAL_VALID_VOTES, getAmount) end
	
	local references = getReferences(electionQid)
	
	local rootSpan, root = beginTable('wikitable sortable', electionQid)
	
	-- Fetch parties data
	local partyNameOverrides = filterArgs(args, 'name_override_Q%d+')
	local partiesData = getAllCandidaciesData({electionQid}, 'party', previousElectionQid, partyNameOverrides, doGroupByParty)
	local totalSeats, totalSeatsChange = getSeatsTotals(partiesData)
	mw.logObject(partiesData)
	
	if #partiesData == 0 then return nil end
	
	local properties = getElectionProperties(partiesData)
	mw.logObject(properties, 'properties')
	
	-- diagramCell (for the parliament diagram)
	local diagramCell = nil
	if (not getStatementQualifier(electionQid, P_COUNTRY, P_APPLIES_TO_PART, getItem)) and (totalSeats > 0) then
		diagramCell = root:tag('th')
	end

	-- Table header
	local headerRow = root:tag('tr')
	
	addHeaderCell(headerRow, (args.partytitle or 'Party'), 2)
	if properties.round1 then
		addHeaderCell(headerRow, 'Votes')
		addHeaderCell(headerRow, '%')
	end
	if properties.previousElection and properties.round1 then addHeaderCell(headerRow, '+/−', nil, nil, 'number') end
	if properties.seats then addHeaderCell(headerRow, 'Seats') end
	if properties.previousElection and properties.seats then addHeaderCell(headerRow, '+/−', nil, nil, 'number') end
	
	cols = cols + 5
	if properties.previousElection then cols = cols + 2 end
	
	if diagramCell then
		fillDiagramCell(
			diagramCell,
			(args['image'] or require('Module:Sandbox/Julio974fr/parliament_diagram').makeParliamentDiagram(partiesData, year)),
			cols
		)
	end
	
	-- Get parties list and make the rows
	for _, party in ipairs(partiesData) do
		local row = root:tag('tr')
		
		if party.qid == 'Q86630688' then --Others
			addCell(row, 'Others', 'left', 2)
			row:addClass('sortbottom')
		else
			addColorCell(row, party.color)
			addCell(row, party.partyWikilink)
		end
		
		if properties.round1 then
			addCell(row, fmt(party.votes1), 'right')
			addCell(row, pct(party.votes1, statistics.totalVotes), 'right')
		end
		if properties.previousElection and properties.round1 then addCell(row, diffPct(party.votes1, totalVotes, party.prevVotes, prevTotalVotes), 'right', nil, nil, diffKeyPct(party.votes1, totalVotes, party.prevVotes, prevTotalVotes)) end
		if properties.seats then addCell(row, party.seats, 'right') end
		if properties.previousElection and properties.seats then addCell(row, diff(party.seats, party.prevSeats), 'right', nil, nil, diffKey(party.seats, party.prevSeats)) end
	end
	
	-- Footer separator and totals row
	if statistics.validVotes then
		local totalRow = addStatRow(root, 'Total', 2, {statistics}, 'validVotes', 'validVotes', 'validVotes', true)
		if properties.previousElection and properties.round1 then addCell(totalRow, '–', 'right') end
		if properties.seats then addCell(totalRow, fmt(totalSeats), 'right') end
		if properties.previousElection and properties.seats then addCell(totalRow, diff(totalSeatsChange, 0), 'right') end
	end
	
	local row = root:tag('tr')
	addHeaderCell(row, '', cols)
	row:addClass('sortbottom')
	
	-- Footer rows (statistics & references)
	if statistics.blankVotes or statistics.invalidVotes then
		addStatRow(root, 'Valid votes', 2, {statistics}, 'validVotes', 'validVotes', 'totalVotes')
		if statistics.blankVotes and not statistics.invalidVotes then
			addStatRow(root, 'Blank and invalid votes', 2, {statistics}, 'blankVotes', 'blankVotes', 'totalVotes')
		else
			addStatRow(root, 'Blank votes', 2, {statistics}, 'blankVotes', 'blankVotes', 'totalVotes')
			addStatRow(root, 'Invalid votes', 2, {statistics}, 'invalidVotes', 'invalidVotes', 'totalVotes')
		end
		addStatRow(root, 'Total votes', 2, {statistics}, 'totalVotes', 'totalVotes', 'validBallots', true)
	end
	if statistics.blankBallots or statistics.invalidBallots then
		if statistics.blankBallots and not statistics.invalidBallots then
			addStatRow(root, 'Blank and invalid ballots', 2, {statistics}, 'blankBallots', 'blankBallots', 'totalBallots')
		else
			addStatRow(root, 'Blank ballots', 2, {statistics}, 'blankBallots', 'blankBallots', 'totalBallots')
			addStatRow(root, 'Invalid ballots', 2, {statistics}, 'invalidBallots', 'invalidBallots', 'totalBallots')
		end
		addStatRow(root, 'Total ballots', 2, {statistics}, 'totalBallots', nil, nil, true)
	end
	addStatRow(root, 'Registered voters/Turnout', 2, {statistics}, 'eligibleVoters', 'totalBallots', 'eligibleVoters')
	
	formatReferencesRow(root, references, electionQid, cols)
	
	return rootSpan
end



function p._ch_majoritarian(args)
	
	local electionQid = args.qid or args.election or args[1]
	
	local cols = 0
	local year = getYear(getStatementValue(electionQid, P_POINT_IN_TIME, getTimestamp))
	local hasIncumbent = false
	
	local references = getReferences(electionQid)
	
	local roundsQid = getRounds(electionQid)
	local firstRoundQid = roundsQid[1] or electionQid
	local secondRoundQid = roundsQid[2]
	
	local statistics = getElectionStatistics(electionQid, firstRoundQid)
	local statistics2 = getElectionStatistics(secondRoundQid)
	mw.logObject({statistics, statistics2}, 'statistics')
	
	local rootSpan, root = beginTable('wikitable sortable', electionQid)
	
	-- Fetch parties data
	local partyNameOverrides = filterArgs(args, 'name_override_Q%d+')
	local candidatesData = getAllCandidaciesData({firstRoundQid, secondRoundQid}, 'candidate', nil, partyNameOverrides, nil)
	mw.logObject(candidatesData)
	
	statistics.validVotes = statistics.validVotes or getSumOfVotes(candidatesData, 1)
	statistics2.validVotes = statistics2.validVotes or getSumOfVotes(candidatesData, 2)
	
	if #candidatesData == 0 then return nil end
	
	local properties = getElectionProperties(candidatesData)
	mw.logObject(properties, 'properties')
	local leftCols = properties.party and 3 or 2

	-- Table header
	local headerRow = root:tag('tr')
	
	if secondRoundQid then -- not using properties.round2 cause it may be useful to show second-round qualifications later on (@todo Add candidate qualifications without votes)
		local secondHeaderRow = root:tag('tr')
		
		addHeaderCell(headerRow, 'Candidate', 2, 2)
		if properties.party then addHeaderCell(headerRow, (args.partytitle or 'Party'), 1, 2) end
		addHeaderCell(headerRow, 'First round', 2)
		addHeaderCell(headerRow, 'Second round', 2)
		
		addHeaderCell(secondHeaderRow, 'Votes')
		addHeaderCell(secondHeaderRow, '%')
		addHeaderCell(secondHeaderRow, 'Votes')
		addHeaderCell(secondHeaderRow, '%')
		cols = cols+7
	else -- If no results yet
		addHeaderCell(headerRow, 'Candidate', 2)
		if properties.party then addHeaderCell(headerRow, (args.partytitle or 'Party')) end
		cols = cols+3
		if properties.round1 then
			addHeaderCell(headerRow, 'Votes')
			addHeaderCell(headerRow, '%')
			cols = cols+2
		end
	end
	
	-- Get parties list and make the rows
	for _, candidate in ipairs(candidatesData) do
		local row = root:tag('tr')
		
		if candidate.isIncumbent then
			candidate.candidateWikilink = candidate.candidateWikilink .. '&nbsp;<sup><abbr title="Incumbent">inc.</abbr></sup>'
			hasIncumbent = true
		end
		
		if candidate.qid == 'Q86630688' then --Others
			addCell(row, 'Others', 'left', leftCols)
			row:addClass('sortbottom')
		elseif properties.party then
			addColorCell(row, candidate.color)
			addCell(row, candidate.candidateWikilink, nil, 1, (candidate.winning and true or false))
			addCell(row, candidate.partyWikilink, 'left')
		else
			addCell(row, candidate.candidateWikilink, nil, 2, (candidate.winning and true or false))
		end
		
		if properties.round1 and candidate.votes1 then
			addCell(row, fmt(candidate.votes1), 'right', 1, (candidate.winning==1))
			addCell(row, pct(candidate.votes1, (statistics.validVotes/statistics.magnitude)), 'right', 1, (candidate.winning==1))
		end
		
		if properties.round2 and candidate.votes2 then
			addCell(row, fmt(candidate.votes2), 'right', 1, (candidate.winning==2))
			addCell(row, pct(candidate.votes2, (statistics2.validVotes/statistics2.magnitude)), 'right', 1, (candidate.winning==2))
		elseif properties.round2 then
			addEmptyCell(row, 2)
		end
		
	end
	
	-- Footer separator and totals row
	addStatRow(root, 'Total', leftCols, {statistics, statistics2}, 'validVotes', 'validVotes', 'validVotes', true)
	
	local row = root:tag('tr')
	addHeaderCell(row, '', cols)
	row:addClass('sortbottom')
	
	-- Footer rows (statistics & references)
	if statistics.blankVotes or statistics.invalidVotes then
		addStatRow(root, 'Valid votes', leftCols, {statistics, statistics2}, 'validVotes', 'validVotes', 'totalVotes')
		if statistics.blankVotes and not statistics.invalidVotes then
			addStatRow(root, 'Blank and invalid votes', leftCols, {statistics, statistics2}, 'blankVotes', 'blankVotes', 'totalVotes')
		else
			addStatRow(root, 'Blank votes', leftCols, {statistics, statistics2}, 'blankVotes', 'blankVotes', 'totalVotes')
			addStatRow(root, 'Invalid votes', leftCols, {statistics, statistics2}, 'invalidVotes', 'invalidVotes', 'totalVotes')
		end
		addStatRow(root, 'Total votes', leftCols, {statistics, statistics2}, 'totalVotes', 'magnitude', nil, true, true)
	end
	if statistics.blankBallots or statistics.invalidBallots then
		if statistics.blankBallots and not statistics.invalidBallots then
			addStatRow(root, 'Blank and invalid ballots', leftCols, {statistics, statistics2}, 'blankBallots', 'blankBallots', 'totalBallots')
		else
			addStatRow(root, 'Blank ballots', leftCols, {statistics, statistics2}, 'blankBallots', 'blankBallots', 'totalBallots')
			addStatRow(root, 'Invalid ballots', leftCols, {statistics, statistics2}, 'invalidBallots', 'invalidBallots', 'totalBallots')
		end
		addStatRow(root, 'Total ballots', leftCols, {statistics, statistics2}, 'totalBallots', nil, nil, true)
	end
	addStatRow(root, 'Registered voters/Turnout', leftCols, {statistics, statistics2}, 'eligibleVoters', 'totalBallots', 'eligibleVoters')
	
	formatReferencesRow(root, references, electionQid, cols, hasIncumbent)
	
	return rootSpan
end





--[[ Wrappers ]]

function p.ch_proportional(frame)
	-- Initialise and populate variables
	local getArgs = require("Module:Arguments").getArgs
	local args = getArgs(frame)
	
	return p._ch_proportional(args)
end

function p.ch_majoritarian(frame)
	-- Initialize and populate variables
	local getArgs = require("Module:Arguments").getArgs
	local args = getArgs(frame)
	
	return p._ch_majoritarian(args)
end

return p