Модуль:Football: различия между версиями
Andrew (обсуждение | вклад) Изменено форматирование |
Andrew (обсуждение | вклад) Нет описания правки |
||
| (не показана 41 промежуточная версия этого же участника) | |||
| Строка 1: | Строка 1: | ||
local p = {} | local p = {} | ||
-- ========================================================= | |||
-- НАСТРОЙКИ | |||
-- ========================================================= | |||
-- Виды карточек в порядке вывода столбцов. | |||
-- key — часть имени параметров: matchN_<key> и <key>_КОД. | |||
local CARD_TYPES = { | |||
{ | |||
key = "yellow", | |||
title = "Жёлтые карточки", | |||
background = "#ffd200" | |||
}, | |||
{ | |||
key = "yellowred", | |||
title = "Удаления за вторую жёлтую карточку", | |||
background = "linear-gradient(135deg, #ffd200 50%, #e30613 50%)" | |||
}, | |||
{ | |||
key = "red", | |||
title = "Прямые красные карточки", | |||
background = "#e30613" | |||
} | |||
} | |||
-- Сколько снятий очков можно привязать к одному матчу: | |||
-- matchN_deduction, matchN_deduction2 … matchN_deduction5 | |||
local MAX_DEDUCTIONS = 5 | |||
-- Части параметра start_КОД (стартовые показатели команды) | |||
-- в порядке записи. label — обозначение части | |||
-- в описании формата и в сообщениях об ошибках. | |||
local START_PARTS = { | |||
{ key = "played", label = "И" }, | |||
{ key = "wins", label = "В" }, | |||
{ key = "draws", label = "Н" }, | |||
{ key = "losses", label = "П" }, | |||
{ key = "goalsFor", label = "МЗ" }, | |||
{ key = "goalsAgainst", label = "МП" }, | |||
{ key = "points", label = "О" }, | |||
{ key = "yellow", label = "ЖК", card = true }, | |||
{ key = "yellowred", label = "2ЖК", card = true }, | |||
{ key = "red", label = "КК", card = true } | |||
} | |||
-- Формат параметра start_КОД: «И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК» | |||
local START_FORMAT | |||
do | |||
local labels = {} | |||
for _, part in ipairs(START_PARTS) do | |||
table.insert(labels, part.label) | |||
end | |||
START_FORMAT = table.concat(labels, ";") | |||
end | |||
-- ========================================================= | -- ========================================================= | ||
| Строка 33: | Строка 91: | ||
return n | return n | ||
end | |||
-- Число для текста: 3 → «3», 1.5 → «1,5» | |||
local function formatNumber(n) | |||
if n == math.floor(n) then | |||
return string.format("%d", n) | |||
end | |||
return (string.gsub(tostring(n), "%.", ",")) | |||
end | |||
-- Форма слова после числа: 1 очко, 2 очка, 5 очков | |||
local function plural(n, one, few, many) | |||
n = math.abs(n) | |||
if n ~= math.floor(n) then | |||
return few | |||
end | |||
local lastTwo = n % 100 | |||
local last = n % 10 | |||
if lastTwo >= 11 and lastTwo <= 14 then | |||
return many | |||
elseif last == 1 then | |||
return one | |||
elseif last >= 2 and last <= 4 then | |||
return few | |||
end | |||
return many | |||
end | |||
-- Пара значений «хозяева:гости», например «2:1» | |||
local function parsePair(value) | |||
local home, away = | |||
trim(value):match("^(%d+)%s*:%s*(%d+)$") | |||
if home and away then | |||
return tonumber(home), tonumber(away) | |||
end | |||
return nil | |||
end | |||
-- Метка сноски: 1 → a, 2 → b … 26 → z, 27 → aa | |||
local function noteLabel(index) | |||
local label = "" | |||
while index > 0 do | |||
local rest = (index - 1) % 26 | |||
label = string.char(97 + rest) .. label | |||
index = math.floor((index - 1) / 26) | |||
end | |||
return label | |||
end | end | ||
| Строка 84: | Строка 202: | ||
status = "ошибка: " .. score | status = "ошибка: " .. score | ||
} | } | ||
end | |||
-- ========================================================= | |||
-- РАЗБОР ЗАПИСИ О МАТЧЕ | |||
-- ========================================================= | |||
-- Значение matchN или h2h_matchN в одном из форматов: | |||
-- тур;круг;хозяева;гости;счёт | |||
-- круг;хозяева;гости;счёт | |||
-- хозяева;гости;счёт | |||
-- Возвращает nil, если частей меньше трёх. | |||
local function parseMatchValue(value) | |||
local parts = split(value, ";") | |||
-- Формат с номером тура | |||
if #parts >= 5 then | |||
return { | |||
tour = parts[1], | |||
round = parts[2], | |||
home = parts[3], | |||
away = parts[4], | |||
score = parts[5] | |||
} | |||
end | |||
-- Формат с номером круга | |||
if #parts == 4 then | |||
return { | |||
tour = "", | |||
round = parts[1], | |||
home = parts[2], | |||
away = parts[3], | |||
score = parts[4] | |||
} | |||
end | |||
-- Формат без номера круга | |||
if #parts == 3 then | |||
return { | |||
tour = "", | |||
round = "", | |||
home = parts[1], | |||
away = parts[2], | |||
score = parts[3] | |||
} | |||
end | |||
return nil | |||
end | end | ||
| Строка 104: | Строка 272: | ||
goalsAgainst = 0, | goalsAgainst = 0, | ||
points = 0 | points = 0, | ||
-- Сколько очков снято всего | |||
deducted = 0, | |||
-- Сноски о снятии очков | |||
notes = {}, | |||
-- Карточки из стартовых показателей и матчей | |||
matchCards = { | |||
yellow = 0, | |||
yellowred = 0, | |||
red = 0 | |||
}, | |||
-- Итоговые карточки (из матчей или из параметров команды) | |||
yellow = 0, | |||
yellowred = 0, | |||
red = 0 | |||
} | } | ||
end | |||
-- Добавляет к показателям команды стартовые | |||
-- (параметр start_КОД), если они указаны | |||
local function addStart(team, start) | |||
if not start then | |||
return | |||
end | |||
team.played = team.played + start.played | |||
team.wins = team.wins + start.wins | |||
team.draws = team.draws + start.draws | |||
team.losses = team.losses + start.losses | |||
team.goalsFor = | |||
team.goalsFor + start.goalsFor | |||
team.goalsAgainst = | |||
team.goalsAgainst + start.goalsAgainst | |||
team.points = | |||
team.points + start.points | |||
for _, cardType in ipairs(CARD_TYPES) do | |||
team.matchCards[cardType.key] = | |||
team.matchCards[cardType.key] + start[cardType.key] | |||
end | |||
end | end | ||
| Строка 127: | Строка 345: | ||
return element | return element | ||
end | |||
-- Значок карточки для заголовка столбца. | |||
-- Рисуется средствами CSS, поэтому не нужны файлы изображений. | |||
local function cardIcon(cardType) | |||
return mw.html.create("span") | |||
:addClass("football-card") | |||
:addClass("football-card-" .. cardType.key) | |||
:attr("title", cardType.title) | |||
:css("display", "inline-block") | |||
:css("width", "0.7em") | |||
:css("height", "0.95em") | |||
:css("vertical-align", "middle") | |||
:css("border-radius", "0.12em") | |||
:css("border", "1px solid rgba(0, 0, 0, 0.35)") | |||
:css("background", cardType.background) | |||
:wikitext("​") | |||
end | end | ||
| Строка 150: | Строка 386: | ||
local pointsLoss = | local pointsLoss = | ||
number(args.points_loss, 0) | number(args.points_loss, 0) | ||
-- ----------------------------------------------------- | |||
-- ЗАГОЛОВКИ РАЗДЕЛОВ | |||
-- ----------------------------------------------------- | |||
-- 3 → «=== Заголовок ===» (по умолчанию), | |||
-- 2 → «== Заголовок ==», 4 → «==== Заголовок ====» | |||
local headingLevel = | |||
math.floor(number(args.heading_level, 3)) | |||
headingLevel = | |||
math.max(2, math.min(6, headingLevel)) | |||
local function heading(text) | |||
local marks = string.rep("=", headingLevel) | |||
-- Заголовок вики-разметки должен стоять | |||
-- на отдельной строке | |||
return "\n" .. marks .. " " .. text .. " " .. marks .. "\n" | |||
end | |||
| Строка 186: | Строка 443: | ||
table.insert(teamOrder, code) | table.insert(teamOrder, code) | ||
end | end | ||
end | |||
-- ----------------------------------------------------- | |||
-- СТАРТОВЫЕ ПОКАЗАТЕЛИ | |||
-- ----------------------------------------------------- | |||
-- start_КОД = И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК — показатели, | |||
-- с которыми команда начинает турнир. Нужны, например, | |||
-- когда после первого этапа таблица делится на группы: | |||
-- таблица каждой группы начинается с итогов первого этапа. | |||
-- Пустая часть считается нулём; пустые И и О вычисляются | |||
-- по победам, ничьим и поражениям. Параметры для кодов, | |||
-- которых нет в teams, не читаются. | |||
-- Возвращает показатели (nil, если параметр не указан) | |||
-- или nil и текст ошибки | |||
local function parseStart(code) | |||
local paramName = "start_" .. code | |||
local raw = trim(args[paramName]) | |||
if raw == "" then | |||
return nil | |||
end | |||
local function fail(text) | |||
return nil, | |||
"'''Ошибка:''' в параметре ''" .. | |||
paramName .. "'' " .. text | |||
end | |||
local parts = split(raw, ";") | |||
if #parts > #START_PARTS then | |||
return fail( | |||
"больше " .. #START_PARTS .. " значений. " .. | |||
"Формат: ''" .. START_FORMAT .. "''." | |||
) | |||
end | |||
local start = { | |||
-- Указаны ли карточки | |||
hasCards = false | |||
} | |||
for index, part in ipairs(START_PARTS) do | |||
local value = parts[index] or "" | |||
if part.key == "points" then | |||
-- Очки могут быть дробными и отрицательными: | |||
-- «12,5», «-3» (пустые вычисляются ниже) | |||
if value ~= "" then | |||
local normalized = | |||
string.gsub(value, ",", ".") | |||
normalized = | |||
string.gsub(normalized, "−", "-") | |||
local amount = tonumber(normalized) | |||
if amount == nil or | |||
amount ~= amount or | |||
math.abs(amount) == math.huge then | |||
return fail( | |||
"очки (О) должны быть числом, " .. | |||
"а указано «" .. value .. "»." | |||
) | |||
end | |||
start.points = amount | |||
end | |||
elseif value == "" then | |||
-- Пустое число игр вычисляется ниже | |||
if part.key ~= "played" then | |||
start[part.key] = 0 | |||
end | |||
elseif value:match("^%d+$") then | |||
start[part.key] = tonumber(value) | |||
if part.card then | |||
start.hasCards = true | |||
end | |||
else | |||
return fail( | |||
"значение " .. part.label .. | |||
" должно быть целым неотрицательным числом, " .. | |||
"а указано «" .. value .. "»." | |||
) | |||
end | |||
end | |||
local results = | |||
start.wins + start.draws + start.losses | |||
if start.played == nil then | |||
start.played = results | |||
elseif start.played ~= results then | |||
return fail( | |||
"число игр (" .. start.played .. ") не равно " .. | |||
"сумме побед, ничьих и поражений (" .. | |||
results .. ")." | |||
) | |||
end | |||
if start.points == nil then | |||
start.points = | |||
start.wins * pointsWin + | |||
start.draws * pointsDraw + | |||
start.losses * pointsLoss | |||
end | |||
return start | |||
end | |||
for _, code in ipairs(teamOrder) do | |||
local start, errorText = parseStart(code) | |||
if errorText then | |||
return errorText | |||
end | |||
if start then | |||
teams[code].start = start | |||
addStart(teams[code], start) | |||
end | |||
end | |||
-- Префикс якорей для сносок. Он должен быть уникальным | |||
-- на странице, поэтому по умолчанию зависит от списка команд. | |||
local idPrefix = trim(args.id) | |||
if idPrefix == "" then | |||
idPrefix = "football" | |||
if mw.hash and mw.hash.hashValue then | |||
local ok, hash = pcall( | |||
mw.hash.hashValue, | |||
"crc32b", | |||
teamsParameter | |||
) | |||
if ok and hash then | |||
idPrefix = idPrefix .. "-" .. hash | |||
end | |||
end | |||
else | |||
-- Символы, которые ломают якорь или ссылку [[#…]] | |||
idPrefix = string.gsub(idPrefix, "[%s%[%]{}|#<>\"'&]+", "_") | |||
end | |||
-- ----------------------------------------------------- | |||
-- УЧЁТ РЕЗУЛЬТАТА И СНЯТИЯ ОЧКОВ | |||
-- ----------------------------------------------------- | |||
-- Добавляет результат сыгранного матча в статистику. | |||
-- teamsMap — итоговые команды или команды «по турам». | |||
local function addResult(teamsMap, match) | |||
local homeTeam = teamsMap[match.home] | |||
local awayTeam = teamsMap[match.away] | |||
homeTeam.played = homeTeam.played + 1 | |||
awayTeam.played = awayTeam.played + 1 | |||
homeTeam.goalsFor = | |||
homeTeam.goalsFor + match.homeGoals | |||
homeTeam.goalsAgainst = | |||
homeTeam.goalsAgainst + match.awayGoals | |||
awayTeam.goalsFor = | |||
awayTeam.goalsFor + match.awayGoals | |||
awayTeam.goalsAgainst = | |||
awayTeam.goalsAgainst + match.homeGoals | |||
if match.homeGoals > match.awayGoals then | |||
homeTeam.wins = homeTeam.wins + 1 | |||
awayTeam.losses = awayTeam.losses + 1 | |||
homeTeam.points = | |||
homeTeam.points + pointsWin | |||
awayTeam.points = | |||
awayTeam.points + pointsLoss | |||
elseif match.homeGoals < match.awayGoals then | |||
awayTeam.wins = awayTeam.wins + 1 | |||
homeTeam.losses = homeTeam.losses + 1 | |||
awayTeam.points = | |||
awayTeam.points + pointsWin | |||
homeTeam.points = | |||
homeTeam.points + pointsLoss | |||
else | |||
homeTeam.draws = homeTeam.draws + 1 | |||
awayTeam.draws = awayTeam.draws + 1 | |||
homeTeam.points = | |||
homeTeam.points + pointsDraw | |||
awayTeam.points = | |||
awayTeam.points + pointsDraw | |||
end | |||
end | |||
-- Снимает очки, привязанные к матчу | |||
-- (независимо от того, сыгран ли он) | |||
local function applyDeductions(teamsMap, match) | |||
for _, deduction in ipairs(match.deductions) do | |||
local team = teamsMap[deduction.team] | |||
team.points = | |||
team.points - deduction.points | |||
team.deducted = | |||
team.deducted + deduction.points | |||
end | |||
end | end | ||
| Строка 194: | Строка 721: | ||
local matches = {} | local matches = {} | ||
-- Все снятия очков в порядке матчей | |||
local deductions = {} | |||
-- Указаны ли где-нибудь карточки | |||
local hasCards = false | |||
local matchNumber = 1 | local matchNumber = 1 | ||
| Строка 204: | Строка 738: | ||
end | end | ||
local | local fields = parseMatchValue(value) | ||
if not fields then | |||
if | |||
return "'''Ошибка:''' матч " .. | return "'''Ошибка:''' матч " .. | ||
| Строка 236: | Строка 747: | ||
end | end | ||
local tour = fields.tour | |||
local round = fields.round | |||
local home = fields.home | |||
local away = fields.away | |||
local score = fields.score | |||
| Строка 263: | Строка 780: | ||
local match = { | local match = { | ||
number = matchNumber, | number = matchNumber, | ||
tour = tour, | |||
round = round, | round = round, | ||
| Строка 273: | Строка 791: | ||
homeGoals = parsed.homeGoals, | homeGoals = parsed.homeGoals, | ||
awayGoals = parsed.awayGoals, | awayGoals = parsed.awayGoals, | ||
score = parsed.score | score = parsed.score, | ||
deductions = {} | |||
} | } | ||
-- ------------------------------------------------- | |||
-- КАРТОЧКИ В МАТЧЕ | |||
-- ------------------------------------------------- | |||
-- matchN_yellow, matchN_yellowred, matchN_red | |||
-- в формате «хозяева:гости» | |||
for _, cardType in ipairs(CARD_TYPES) do | |||
local paramName = | |||
"match" .. matchNumber .. "_" .. cardType.key | |||
local raw = trim(args[paramName]) | |||
if raw ~= "" then | |||
local homeCards, awayCards = parsePair(raw) | |||
if homeCards == nil then | |||
return "'''Ошибка:''' параметр ''" .. | |||
paramName .. | |||
"'' имеет неправильный формат «" .. | |||
raw .. | |||
"». Укажите карточки хозяев и гостей " .. | |||
"через двоеточие, например ''2:1''." | |||
end | |||
local homeCardsTable = teams[home].matchCards | |||
local awayCardsTable = teams[away].matchCards | |||
homeCardsTable[cardType.key] = | |||
homeCardsTable[cardType.key] + homeCards | |||
awayCardsTable[cardType.key] = | |||
awayCardsTable[cardType.key] + awayCards | |||
hasCards = true | |||
end | |||
end | |||
-- ------------------------------------------------- | |||
-- СНЯТИЕ ОЧКОВ ЗА МАТЧ | |||
-- ------------------------------------------------- | |||
-- matchN_deduction = код;очки;причина | |||
for i = 1, MAX_DEDUCTIONS do | |||
local paramName = | |||
"match" .. matchNumber .. "_deduction" | |||
if i > 1 then | |||
paramName = paramName .. i | |||
end | |||
local raw = trim(args[paramName]) | |||
if raw ~= "" then | |||
local deductionParts = split(raw, ";") | |||
local code = deductionParts[1] | |||
-- Допускается и «1,5», и «1.5» | |||
local amount = tonumber( | |||
(string.gsub(deductionParts[2] or "", ",", ".")) | |||
) | |||
if code ~= home and code ~= away then | |||
return "'''Ошибка:''' в параметре ''" .. | |||
paramName .. | |||
"'' должна быть указана команда матча " .. | |||
matchNumber .. " ('''" .. home .. | |||
"''' или '''" .. away .. | |||
"'''), а указано «" .. code .. "»." | |||
end | |||
if amount == nil or | |||
amount == 0 or | |||
amount ~= amount or | |||
math.abs(amount) == math.huge then | |||
return "'''Ошибка:''' в параметре ''" .. | |||
paramName .. | |||
"'' не указано, сколько очков снято." | |||
end | |||
-- Причина — всё, что после второй «;» | |||
local reason = | |||
table.concat(deductionParts, "; ", 3) | |||
reason = string.gsub(reason, "[%s%.]+$", "") | |||
local deduction = { | |||
team = code, | |||
points = math.abs(amount), | |||
reason = reason, | |||
match = match | |||
} | |||
table.insert(match.deductions, deduction) | |||
table.insert(deductions, deduction) | |||
end | |||
end | |||
table.insert(matches, match) | table.insert(matches, match) | ||
| Строка 284: | Строка 921: | ||
if parsed.played then | if parsed.played then | ||
addResult(teams, match) | |||
end | |||
applyDeductions(teams, match) | |||
matchNumber = matchNumber + 1 | |||
end | |||
-- ===================================================== | |||
-- МАТЧИ ДЛЯ ЛИЧНЫХ ВСТРЕЧ | |||
-- ===================================================== | |||
-- h2h_matchN — матчи, сыгранные до этого турнира (например, | |||
-- в первом этапе), в тех же форматах, что и matchN. | |||
-- Они учитываются только в критерии «личные» и не влияют | |||
-- на показатели команд, шахматку и таблицу по турам. | |||
-- Матчи с командами, которых нет в teams, пропускаются, | |||
-- поэтому можно вставить все матчи первого этапа. | |||
local h2hMatches = {} | |||
local h2hNumber = 1 | |||
while true do | |||
local paramName = "h2h_match" .. h2hNumber | |||
local value = trim(args[paramName]) | |||
if value == "" then | |||
break | |||
end | |||
local fields = parseMatchValue(value) | |||
if not fields then | |||
return "'''Ошибка:''' параметр ''" .. | |||
paramName .. | |||
"'' имеет неправильный формат." | |||
end | |||
local parsed = parseScore(fields.score) | |||
if parsed.played and | |||
teams[fields.home] and | |||
teams[fields.away] then | |||
table.insert(h2hMatches, { | |||
home = fields.home, | |||
away = fields.away, | |||
homeGoals = parsed.homeGoals, | |||
awayGoals = parsed.awayGoals | |||
}) | |||
end | |||
h2hNumber = h2hNumber + 1 | |||
end | |||
-- ===================================================== | |||
-- КАРТОЧКИ КОМАНД | |||
-- ===================================================== | |||
-- Параметры yellow_КОД, yellowred_КОД, red_КОД задают | |||
-- итог команды. Если указан хотя бы один из них, карточки | |||
-- команды берутся только из этих параметров (неуказанные | |||
-- считаются нулём), а карточки из матчей и стартовых | |||
-- показателей не учитываются. | |||
for _, code in ipairs(teamOrder) do | |||
local team = teams[code] | |||
local override = {} | |||
local hasOverride = false | |||
for _, cardType in ipairs(CARD_TYPES) do | |||
local paramName = cardType.key .. "_" .. code | |||
local raw = trim(args[paramName]) | |||
if raw ~= "" then | |||
if not raw:match("^%d+$") then | |||
return "'''Ошибка:''' параметр ''" .. | |||
paramName .. | |||
"'' должен быть целым неотрицательным " .. | |||
"числом, а указано «" .. raw .. "»." | |||
end | |||
override[cardType.key] = tonumber(raw) | |||
hasOverride = true | |||
end | |||
end | |||
for _, cardType in ipairs(CARD_TYPES) do | |||
if hasOverride then | |||
team[cardType.key] = | |||
override[cardType.key] or 0 | |||
else | |||
team[cardType.key] = | |||
team.matchCards[cardType.key] | |||
end | end | ||
end | end | ||
if hasOverride or | |||
(team.start and team.start.hasCards) then | |||
hasCards = true | |||
end | |||
end | end | ||
| Строка 366: | Строка 1062: | ||
-- ===================================================== | -- ===================================================== | ||
-- | -- Добавляет сыгранный матч к статистике личных встреч | ||
-- | -- команды code, если обе команды матча входят в группу. | ||
local function addHeadToHeadMatch(result, code, group, match) | |||
if not (group[match.home] and group[match.away]) then | |||
return | |||
end | |||
local | local scored | ||
local conceded | |||
if match.home == code then | |||
scored = match.homeGoals | |||
conceded = match.awayGoals | |||
elseif match.away == code then | |||
scored = match.awayGoals | |||
conceded = match.homeGoals | |||
else | |||
return | |||
end | |||
result.goalsFor = | |||
result.goalsFor + scored | |||
result.goalsAgainst = | |||
result.goalsAgainst + conceded | |||
if scored > conceded then | |||
result.points = | |||
result.points + pointsWin | |||
result.wins = | |||
result.wins + 1 | |||
elseif scored == conceded then | |||
result.points = | |||
result.points + pointsDraw | |||
else | |||
result.points = | |||
result.points + pointsLoss | |||
end | |||
end | |||
-- Возвращает статистику команды только в матчах | |||
-- против команд из указанной группы. | |||
-- Матчи h2h_matchN учитываются всегда, а матчи турнира — | |||
-- если задан lastTour, только с номером тура не больше lastTour. | |||
-- Снятие очков на личные встречи не влияет. | |||
local function headToHead(code, group, lastTour) | |||
local result = { | |||
points = 0, | |||
goalsFor = 0, | |||
goalsAgainst = 0, | |||
goalDifference = 0, | |||
wins = 0 | |||
} | |||
-- Матчи, сыгранные до этого турнира | |||
for _, match in ipairs(h2hMatches) do | |||
addHeadToHeadMatch(result, code, group, match) | |||
end | |||
for _, match in ipairs(matches) do | |||
local inRange = true | |||
if lastTour then | |||
local tourNumber = tonumber(match.tour) | |||
inRange = | |||
tourNumber ~= nil and tourNumber <= lastTour | |||
end | |||
if match.played and inRange then | |||
addHeadToHeadMatch(result, code, group, match) | |||
end | end | ||
| Строка 497: | Строка 1199: | ||
-- ----------------------------------------------------- | -- ----------------------------------------------------- | ||
local function | -- Возвращает функцию сравнения для table.sort. | ||
-- teamsMap — команды, среди которых ищутся равные по очкам | |||
-- (для критерия «личные»); lastTour — последний | |||
-- учитываемый тур (nil — все матчи). | |||
local function makeComparator(teamsMap, lastTour) | |||
return function(a, b) | |||
for _, criterion in ipairs(criteria) do | |||
-- ----------------------------------------- | |||
-- ОЧКИ | |||
-- ----------------------------------------- | |||
if criterion == "очки" then | |||
if a.points ~= b.points then | |||
return a.points > b.points | |||
end | |||
-- ----------------------------------------- | |||
-- РАЗНИЦА МЯЧЕЙ | |||
-- ----------------------------------------- | |||
elseif criterion == "разница" then | |||
if a.goalDifference ~= b.goalDifference then | |||
return a.goalDifference > b.goalDifference | |||
end | |||
-- ----------------------------------------- | |||
-- ЗАБИТЫЕ МЯЧИ | |||
-- ----------------------------------------- | |||
elseif criterion == "забитые" then | |||
if a.goalsFor ~= b.goalsFor then | |||
return a.goalsFor > b.goalsFor | |||
end | |||
if a. | -- ----------------------------------------- | ||
return | -- ПОБЕДЫ | ||
-- ----------------------------------------- | |||
elseif criterion == "победы" then | |||
if a.wins ~= b.wins then | |||
return a.wins > b.wins | |||
end | |||
-- ----------------------------------------- | |||
-- ИГРЫ | |||
-- ----------------------------------------- | |||
elseif criterion == "игры" then | |||
if a.played ~= b.played then | |||
return a.played > b.played | |||
end | |||
-- ----------------------------------------- | |||
-- ЛИЧНЫЕ | |||
-- ----------------------------------------- | |||
elseif criterion == "личные" then | |||
-- Все команды с таким же | |||
-- количеством очков, как у a и b. | |||
local group = {} | |||
for _, code in ipairs(teamOrder) do | |||
if teamsMap[code].points == a.points then | |||
group[code] = true | |||
end | |||
end | |||
local ha = headToHead(a.code, group, lastTour) | |||
local hb = headToHead(b.code, group, lastTour) | |||
if ha.points ~= hb.points then | |||
return ha.points > hb.points | |||
end | |||
if ha.goalDifference ~= hb.goalDifference then | |||
return ha.goalDifference > hb.goalDifference | |||
end | |||
if ha.goalsFor ~= hb.goalsFor then | |||
return ha.goalsFor > hb.goalsFor | |||
end | |||
if ha.wins ~= hb.wins then | |||
return ha.wins > hb.wins | |||
end | |||
-- ----------------------------------------- | |||
-- НАЗВАНИЕ | |||
-- ----------------------------------------- | |||
elseif criterion == "название" then | |||
local aname = | |||
mw.text.unstrip(a.name) | |||
local bname = | |||
mw.text.unstrip(b.name) | |||
if aname ~= bname then | |||
return aname < bname | |||
end | |||
end | end | ||
end | |||
-- Последний резервный критерий — | |||
-- код команды. | |||
return a.code < b.code | |||
end | |||
end | |||
-- ===================================================== | |||
-- НУМЕРАЦИЯ МЕСТ | |||
-- ===================================================== | |||
-- first_place — номер первого места таблицы. Например, | |||
-- в таблице группы за 7–14 места первая строка — 7-е место. | |||
-- Места в highlightN и progress_highlightN указываются | |||
-- в этой же нумерации. | |||
local placeOffset = 0 | |||
local firstPlace = trim(args.first_place) | |||
if firstPlace ~= "" then | |||
if not firstPlace:match("^%d+$") or | |||
tonumber(firstPlace) < 1 then | |||
return "'''Ошибка:''' параметр ''first_place'' должен " .. | |||
"быть целым положительным числом, а указано «" .. | |||
firstPlace .. "»." | |||
end | |||
placeOffset = tonumber(firstPlace) - 1 | |||
end | |||
-- ===================================================== | |||
-- Выделение победителей и проигравших | |||
-- ===================================================== | |||
-- highlightN = место;количество;класс;текст | |||
local highlights = {} | |||
-- Есть ли текст хотя бы у одного выделения | |||
local hasHighlightText = false | |||
for i = 1, 15 do | |||
local hl = trim(args["highlight" .. i]) | |||
if hl == "" then | |||
break | |||
end | |||
local parts = split(hl, ";") | |||
local place = tonumber(parts[1]) | |||
if place then | |||
-- Номер строки таблицы с учётом first_place | |||
local row = math.floor(place) - placeOffset | |||
local count = | |||
math.max(1, math.floor(tonumber(parts[2]) or 1)) | |||
-- Зона, которая начинается выше таблицы, | |||
-- обрезается по первой строке | |||
if row < 1 then | |||
count = count + row - 1 | |||
row = 1 | |||
end | |||
-- Зоны целиком за пределами таблицы пропускаются | |||
if count >= 1 and row <= #teamOrder then | |||
local highlight = { | |||
place = row, | |||
count = count, | |||
color = parts[3] or "", | |||
-- Текст может сам содержать «;» | |||
text = table.concat(parts, "; ", 4) | |||
} | |||
if highlight.text ~= "" then | |||
hasHighlightText = true | |||
end | end | ||
table.insert(highlights, highlight) | |||
end | |||
end | |||
end | |||
table.sort(highlights, function(a, b) | |||
return a.place < b.place | |||
end) | |||
-- ----------------------------------------------------- | |||
-- Режим отображения шахматки | |||
-- ----------------------------------------------------- | |||
-- По умолчанию первая строчка - домашние игры, вторая строчка - гостевые | |||
-- Если resultsMode = round, то первая строчка - 1 круг, вторая строчка - 2 круг | |||
local resultsMode = 0 | |||
if args["resultsMode"] ~= nil and args["resultsMode"] ~= "" then | |||
if args["resultsMode"] == "round" then | |||
resultsMode = 1 | |||
end | |||
end | |||
-- rounds = 1 — однокруговой турнир: у каждой команды в шахматке | |||
-- одна строка, в ячейке — единственный матч пары (resultsMode | |||
-- при этом не действует). rounds = 2 или пусто — двухкруговой. | |||
local roundsParameter = trim(args.rounds) | |||
if roundsParameter ~= "" and | |||
roundsParameter ~= "1" and | |||
roundsParameter ~= "2" then | |||
return "'''Ошибка:''' параметр ''rounds'' может быть равен " .. | |||
"1 (однокруговой турнир) или 2 (двухкруговой), " .. | |||
"а указано «" .. roundsParameter .. "»." | |||
end | |||
local singleRound = roundsParameter == "1" | |||
-- ===================================================== | |||
-- ПРОГРЕСС ПО ТУРАМ | |||
-- ===================================================== | |||
local function buildProgress() | |||
local progress = {} | |||
-- Собираем список туров | |||
local tours = {} | |||
for _, match in ipairs(matches) do | |||
local tourNumber = tonumber(match.tour) | |||
if tourNumber then | |||
tours[tourNumber] = true | |||
end | |||
end | |||
local tourNumbers = {} | |||
for tourNumber in pairs(tours) do | |||
table.insert(tourNumbers, tourNumber) | |||
end | |||
table.sort(tourNumbers) | |||
-- Таблица, которая последовательно | |||
-- пересчитывается после каждого тура | |||
local currentTeams = {} | |||
for _, code in ipairs(teamOrder) do | |||
currentTeams[code] = newTeam( | |||
code, | |||
teams[code].name | |||
) | |||
-- Стартовые показатели действуют с первого тура | |||
addStart(currentTeams[code], teams[code].start) | |||
end | |||
for _, tourNumber in ipairs(tourNumbers) do | |||
-- Добавляем матчи данного тура | |||
for _, match in ipairs(matches) do | |||
if tonumber(match.tour) == tourNumber then | |||
if | if match.played then | ||
addResult(currentTeams, match) | |||
end | end | ||
-- Снятие очков действует с этого тура | |||
applyDeductions(currentTeams, match) | |||
end | end | ||
end | |||
-- Разница мячей | |||
for _, code in ipairs(teamOrder) do | |||
local team = currentTeams[code] | |||
team.goalDifference = | |||
team.goalsFor - team.goalsAgainst | |||
end | |||
-- | -- Таблица после этого тура | ||
local tourStandings = {} | |||
for _, code in ipairs(teamOrder) do | |||
table.insert( | |||
tourStandings, | |||
currentTeams[code] | |||
) | |||
end | |||
table.sort( | |||
tourStandings, | |||
makeComparator(currentTeams, tourNumber) | |||
) | |||
progress[tourNumber] = {} | |||
for position, team in ipairs(tourStandings) do | |||
progress[tourNumber][team.code] = | |||
position | |||
end | end | ||
| Строка 618: | Строка 1562: | ||
return progress, tourNumbers | |||
end | end | ||
| Строка 635: | Строка 1577: | ||
end | end | ||
table.sort(standings, | table.sort(standings, makeComparator(teams, nil)) | ||
local teamCount = #standings | |||
-- ===================================================== | |||
-- Подсветка | |||
-- ===================================================== | |||
-- Цвет строк | |||
for _, hl in ipairs(highlights) do | |||
local first = math.max(hl.place, 1) | |||
local last = math.min(hl.place + hl.count - 1, teamCount) | |||
for position = first, last do | |||
standings[position].highlightColor = hl.color | |||
end | |||
end | |||
-- Ячейки столбца «Повышение или выбывание». | |||
-- Каждая зона — одна объединённая ячейка с текстом, | |||
-- места между зонами — объединённые пустые ячейки. | |||
if hasHighlightText then | |||
local position = 1 | |||
local index = 1 | |||
while position <= teamCount do | |||
-- Пропускаем зоны, которые начинаются выше | |||
-- текущей строки (пересекаются с предыдущей) | |||
while highlights[index] and | |||
highlights[index].place < position do | |||
index = index + 1 | |||
end | |||
local hl = highlights[index] | |||
local span | |||
local text | |||
if hl and hl.place == position then | |||
span = math.min(hl.count, teamCount - position + 1) | |||
text = hl.text | |||
index = index + 1 | |||
else | |||
local nextPlace = teamCount + 1 | |||
if hl then | |||
nextPlace = math.min(hl.place, teamCount + 1) | |||
end | |||
span = nextPlace - position | |||
text = "" | |||
end | |||
standings[position].zoneCell = { | |||
text = text, | |||
span = span | |||
} | |||
position = position + span | |||
end | |||
end | |||
-- ===================================================== | |||
-- ПРИМЕЧАНИЯ О СНЯТИИ ОЧКОВ | |||
-- ===================================================== | |||
-- Текст примечания, например: | |||
-- «С команды «Спартак» снято 3 очка в 5-м туре | |||
-- (матч «Спартак» — «Локомотив») за неявку на матч.» | |||
local function deductionText(deduction) | |||
local match = deduction.match | |||
local text = | |||
"С команды «" .. teams[deduction.team].name .. | |||
"» снято " .. formatNumber(deduction.points) .. " " .. | |||
plural(deduction.points, "очко", "очка", "очков") | |||
local tourNumber = tonumber(match.tour) | |||
if tourNumber then | |||
text = text .. " в " .. | |||
formatNumber(tourNumber) .. "-м туре" | |||
end | |||
text = text .. | |||
" (матч «" .. teams[match.home].name .. | |||
"» — «" .. teams[match.away].name .. "»)" | |||
if deduction.reason ~= "" then | |||
text = text .. " " .. deduction.reason | |||
end | |||
return text .. "." | |||
end | |||
-- Сноски нумеруются сверху вниз по итоговой таблице | |||
local notes = {} | |||
for _, team in ipairs(standings) do | |||
for _, deduction in ipairs(deductions) do | |||
if deduction.team == team.code then | |||
local label = noteLabel(#notes + 1) | |||
local note = { | |||
label = label, | |||
id = idPrefix .. "-note-" .. label, | |||
refId = idPrefix .. "-ref-" .. label, | |||
text = deductionText(deduction) | |||
} | |||
deduction.note = note | |||
table.insert(notes, note) | |||
table.insert(team.notes, note) | |||
end | |||
end | |||
end | |||
-- Сноска вида [a]. Сноска в итоговой таблице получает id, | |||
-- чтобы из примечания можно было вернуться к ней (↑). | |||
local function noteMarker(note, withAnchor) | |||
local marker = | |||
mw.html.create("sup") | |||
:addClass("reference") | |||
:addClass("football-note-ref") | |||
:wikitext( | |||
"[[#" .. note.id .. "|[" .. | |||
note.label .. "]]]" | |||
) | |||
if withAnchor then | |||
marker:attr("id", note.refId) | |||
end | |||
return marker | |||
end | |||
-- ===================================================== | -- ===================================================== | ||
| Строка 641: | Строка 1739: | ||
-- ===================================================== | -- ===================================================== | ||
local root | local root = | ||
mw.html.create("div") | mw.html.create("div") | ||
:addClass("football- | :addClass("football-tables") | ||
| Строка 654: | Строка 1748: | ||
:addClass("wikitable") | :addClass("wikitable") | ||
:addClass("football-standings") | :addClass("football-standings") | ||
:css("text-align", "center") | |||
| Строка 674: | Строка 1769: | ||
} | } | ||
for _, | for _, title in ipairs(headings) do | ||
standingsHeader:node( | |||
cell("th", title) | |||
) | |||
end | |||
-- Столбцы карточек выводятся, только если | |||
-- карточки где-нибудь указаны | |||
if hasCards then | |||
for _, cardType in ipairs(CARD_TYPES) do | |||
standingsHeader:node( | |||
mw.html.create("th") | |||
:addClass("football-cards") | |||
:attr("title", cardType.title) | |||
:node(cardIcon(cardType)) | |||
) | |||
end | |||
end | |||
if hasHighlightText then | |||
standingsHeader:node( | standingsHeader:node( | ||
cell("th", | cell("th", "Повышение или выбывание") | ||
) | ) | ||
| Строка 692: | Строка 1812: | ||
mw.html.create("tr") | mw.html.create("tr") | ||
if team.highlightColor and team.highlightColor ~= "" then | |||
row:addClass("highlight"):addClass(team.highlightColor) | |||
end | |||
row:node( | row:node( | ||
cell("td", position) | cell("td", position + placeOffset) | ||
) | ) | ||
-- Название и сноски о снятии очков | |||
local teamName = | |||
cell("td", team.name) | |||
:css("text-align", "left") | |||
for _, note in ipairs(team.notes) do | |||
teamName:node( | |||
noteMarker(note, true) | |||
) | |||
end | |||
row:node( | row:node( | ||
teamName | |||
) | ) | ||
| Строка 706: | Строка 1841: | ||
cell("td", team.played) | cell("td", team.played) | ||
) | ) | ||
row:node( | row:node( | ||
cell("td", team.wins) | cell("td", team.wins) | ||
) | ) | ||
row:node( | row:node( | ||
cell("td", team.draws) | cell("td", team.draws) | ||
) | ) | ||
row:node( | row:node( | ||
cell("td", team.losses) | cell("td", team.losses) | ||
) | ) | ||
row:node( | row:node( | ||
cell( "td", team.goalsFor) | cell("td", team.goalsFor) | ||
) | ) | ||
row:node( | row:node( | ||
cell("td",team.goalsAgainst) | cell("td", team.goalsAgainst) | ||
) | ) | ||
local gd | local gd | ||
| Строка 738: | Строка 1870: | ||
gd = tostring(team.goalDifference) | gd = tostring(team.goalDifference) | ||
end | end | ||
row:node( | row:node( | ||
| Строка 744: | Строка 1875: | ||
) | ) | ||
local pointsCell = | |||
cell("td", formatNumber(team.points)) | |||
if team.deducted > 0 then | |||
pointsCell:attr( | |||
"title", | |||
"Снято " .. formatNumber(team.deducted) .. " " .. | |||
plural(team.deducted, "очко", "очка", "очков") | |||
) | |||
end | |||
row:node( | row:node( | ||
pointsCell | |||
) | ) | ||
if hasCards then | |||
for _, cardType in ipairs(CARD_TYPES) do | |||
row:node( | |||
cell("td", team[cardType.key], "football-cards") | |||
) | |||
end | |||
end | |||
if hasHighlightText and team.zoneCell then | |||
local zone = | |||
cell("td", team.zoneCell.text) | |||
if team.zoneCell.span > 1 then | |||
zone:attr("rowspan", team.zoneCell.span) | |||
end | |||
row:node( | |||
zone | |||
) | |||
end | |||
| Строка 773: | Строка 1946: | ||
root:node(info) | root:node(info) | ||
-- ===================================================== | -- ===================================================== | ||
-- | -- ПРИМЕЧАНИЯ ПОД ТАБЛИЦЕЙ | ||
-- ===================================================== | -- ===================================================== | ||
if #notes > 0 then | |||
local notesBlock = | |||
mw.html.create("div") | |||
:addClass("football-info") | |||
:addClass("football-notes") | |||
:wikitext("Примечания:") | |||
local list = | |||
notesBlock:tag("ol") | |||
:css("list-style-type", "lower-alpha") | |||
for _, note in ipairs(notes) do | |||
local item = | |||
list:tag("li") | |||
:attr("id", note.id) | |||
item:tag("span") | |||
:addClass("mw-cite-backlink") | |||
:wikitext("[[#" .. note.refId .. "|↑]]") | |||
item:wikitext(" ") | |||
item:tag("span") | |||
:addClass("reference-text") | |||
:wikitext(note.text) | |||
end | |||
root:node(notesBlock) | |||
end | |||
-- ===================================================== | |||
-- РЕЗУЛЬТАТЫ | |||
-- ===================================================== | |||
root: | root:wikitext(heading("Результаты")) | ||
| Строка 793: | Строка 1998: | ||
:addClass("wikitable") | :addClass("wikitable") | ||
:addClass("football-results") | :addClass("football-results") | ||
:css("text-align", "center") | |||
| Строка 821: | Строка 2026: | ||
-- Матрица результатов | -- Матрица результатов | ||
-- Матч для ячейки шахматки. | |||
-- eitherVenue = false — только матч на поле homeCode, | |||
-- true — матч этой пары на любом поле (однокруговой турнир). | |||
-- Если подходящих матчей несколько, берётся первый сыгранный, | |||
-- если сыгранных нет — первый с пометкой («перенесён» и т. п.), | |||
-- иначе просто первый. | |||
local function findMatch(homeCode, awayCode, eitherVenue) | |||
local found | |||
local foundRank = 0 | |||
for _, match in ipairs(matches) do | |||
local suits = | |||
(match.home == homeCode and match.away == awayCode) or | |||
(eitherVenue and | |||
match.home == awayCode and match.away == homeCode) | |||
if suits then | |||
local rank = 1 | |||
if match.played then | |||
rank = 3 | |||
elseif match.status ~= "" then | |||
rank = 2 | |||
end | |||
if rank > foundRank then | |||
found = match | |||
foundRank = rank | |||
end | |||
end | |||
end | |||
return found | |||
end | |||
-- Ячейка шахматки для команды строки rowCode. | |||
-- Счёт записан как «хозяева:гости», | |||
-- а цвет показывает результат команды строки. | |||
local function makeResultCell(match, rowCode) | |||
local resultCell = | |||
mw.html.create("td") | |||
if not match then | |||
return resultCell:wikitext("—") | |||
end | |||
if match.played then | |||
local scored = match.homeGoals | |||
local conceded = match.awayGoals | |||
if match.away == rowCode then | |||
scored = match.awayGoals | |||
conceded = match.homeGoals | |||
end | |||
if scored > conceded then | |||
resultCell | |||
:addClass("ui") | |||
:addClass("green") | |||
elseif scored < conceded then | |||
resultCell | |||
:addClass("ui") | |||
:addClass("red") | |||
else | |||
resultCell | |||
:addClass("ui") | |||
:addClass("yellow") | |||
end | |||
resultCell:wikitext(match.score) | |||
elseif match.status ~= "" then | |||
resultCell:wikitext(match.status) | |||
else | |||
resultCell:wikitext("—") | |||
end | |||
return resultCell | |||
end | |||
-- Строк у команды: в двухкруговом турнире две | |||
-- (дома и в гостях или 1-й и 2-й круг), в однокруговом — одна | |||
local rowsPerTeam = 2 | |||
if singleRound then | |||
rowsPerTeam = 1 | |||
end | |||
for _, homeCode in ipairs(teamOrder) do | for _, homeCode in ipairs(teamOrder) do | ||
local | local homeRow = | ||
mw.html.create("tr") | |||
local awayRow = | |||
mw.html.create("tr") | mw.html.create("tr") | ||
local | local teamHeader = | ||
cell( | |||
"th", | |||
teams[homeCode].name, | |||
) | "football-team-header" | ||
) | |||
if rowsPerTeam > 1 then | |||
teamHeader:attr("rowspan", rowsPerTeam) | |||
end | |||
teamHeader:css("text-align", "right") | |||
homeRow:node(teamHeader) | |||
| Строка 841: | Строка 2153: | ||
if homeCode == awayCode then | if homeCode == awayCode then | ||
local diagonal = cell("td", "—", "football-diagonal") | |||
if rowsPerTeam > 1 then | |||
diagonal:attr("rowspan", rowsPerTeam) | |||
end | |||
homeRow:node( | |||
diagonal | |||
) | |||
elseif singleRound then | |||
-- Единственный матч пары, на чьём бы поле он ни был | |||
homeRow:node( | |||
makeResultCell( | |||
findMatch(homeCode, awayCode, true), | |||
homeCode | |||
) | |||
) | ) | ||
else | else | ||
local homeMatch = findMatch(homeCode, awayCode, false) | |||
local awayMatch = findMatch(awayCode, homeCode, false) | |||
local homeResultCell = makeResultCell(homeMatch, homeCode) | |||
local awayResultCell = makeResultCell(awayMatch, homeCode) | |||
-- Нужно ли поменять местами гостевую и домашнюю игры: | |||
-- в режиме по кругам первая строка — матч 1-го круга, | |||
-- вторая — матч 2-го круга | |||
local changeOrder = | |||
resultsMode == 1 and ( | |||
(homeMatch ~= nil and homeMatch.round == "2") or | |||
(awayMatch ~= nil and awayMatch.round == "1") | |||
) | |||
if changeOrder then | |||
homeRow:node(awayResultCell) | |||
awayRow:node(homeResultCell) | |||
else | |||
homeRow:node(homeResultCell) | |||
awayRow:node(awayResultCell) | |||
end | |||
end | |||
end | |||
resultsTable:node(homeRow) | |||
if rowsPerTeam > 1 then | |||
resultsTable:node(awayRow) | |||
end | |||
end | |||
root:node(resultsTable) | |||
-- ===================================================== | |||
-- ПРОГРЕСС ПО ТУРАМ | |||
-- ===================================================== | |||
local showProgress = trim(args.show_progress) == "yes" | |||
local progress | |||
local tourNumbers | |||
if showProgress then | |||
progress, tourNumbers = | |||
buildProgress() | |||
root:wikitext(heading("Положение команд по турам")) | |||
-- Без номеров туров строить нечего | |||
if #tourNumbers == 0 then | |||
root:node( | |||
mw.html.create("div") | |||
:addClass("football-info") | |||
:wikitext( | |||
"''Нет матчей с номером тура. " .. | |||
"Для этой таблицы матчи нужно указывать " .. | |||
"в формате «тур;круг;хозяева;гости;счёт».''" | |||
) | |||
) | |||
showProgress = false | |||
end | |||
end | |||
if showProgress then | |||
local progressTable = | |||
mw.html.create("table") | |||
:addClass("wikitable") | |||
:addClass("football-progress") | |||
:css("text-align", "center") | |||
-- ------------------------------------------------- | |||
-- Цвета мест | |||
-- ------------------------------------------------- | |||
local progressHighlights = {} | |||
for i = 1, 15 do | |||
local value = | |||
trim(args["progress_highlight" .. i]) | |||
if value ~= "" then | |||
local parts = | |||
split(value, ";") | |||
local place = | |||
tonumber(parts[1]) | |||
local color = | |||
parts[2] or "" | |||
if place and color ~= "" then | |||
progressHighlights[place] = | |||
color | |||
end | |||
end | |||
end | |||
-- ------------------------------------------------- | |||
-- Сноски о снятии очков: тур → команда → сноски | |||
-- ------------------------------------------------- | |||
local progressNotes = {} | |||
for _, deduction in ipairs(deductions) do | |||
local tourNumber = tonumber(deduction.match.tour) | |||
if tourNumber and deduction.note then | |||
progressNotes[tourNumber] = | |||
progressNotes[tourNumber] or {} | |||
local teamNotes = | |||
progressNotes[tourNumber][deduction.team] or {} | |||
table.insert(teamNotes, deduction.note) | |||
progressNotes[tourNumber][deduction.team] = | |||
teamNotes | |||
end | |||
end | |||
-- ------------------------------------------------- | |||
-- Заголовок | |||
-- ------------------------------------------------- | |||
local progressHeader = | |||
mw.html.create("tr") | |||
progressHeader:node( | |||
cell("th", "Команда") | |||
:attr("rowspan", 2) | |||
) | |||
progressHeader:node( | |||
cell("th", "Тур") | |||
:attr("colspan", #tourNumbers) | |||
) | |||
progressTable:node(progressHeader) | |||
local tourHeader = | |||
mw.html.create("tr") | |||
for _, tourNumber in ipairs(tourNumbers) do | |||
tourHeader:node( | |||
cell( | |||
"th", | |||
tourNumber | |||
) | |||
) | |||
end | |||
progressTable:node(tourHeader) | |||
-- ------------------------------------------------- | |||
-- Строки команд | |||
-- ------------------------------------------------- | |||
for _, code in ipairs(teamOrder) do | |||
local row = | |||
mw.html.create("tr") | |||
row:node( | |||
cell( | |||
"th", | |||
teams[code].name | |||
) | |||
) | |||
for _, tourNumber in ipairs(tourNumbers) do | |||
-- Место с учётом first_place | |||
local position = | |||
progress[tourNumber][code] + placeOffset | |||
local resultCell = | local resultCell = | ||
cell("td", position) | |||
local color = | |||
progressHighlights[position] | |||
if color and color ~= "" then | |||
resultCell | |||
:addClass("highlight") | |||
:addClass(color) | |||
end | |||
-- В туре, где сняты очки, — та же сноска, | |||
-- что и в итоговой таблице | |||
local cellNotes = | |||
progressNotes[tourNumber] and | |||
progressNotes[tourNumber][code] | |||
if cellNotes then | |||
resultCell:addClass("football-deduction") | |||
for _, note in ipairs(cellNotes) do | |||
resultCell:node( | |||
noteMarker(note, false) | |||
) | |||
end | |||
end | end | ||
| Строка 902: | Строка 2422: | ||
end | end | ||
progressTable:node(row) | |||
end | |||
root:node(progressTable) | |||
end | |||
return tostring(root) | return tostring(root) | ||
Текущая версия от 14:26, 3 октября 2026
Модуль строит для кругового футбольного турнира — двухкругового или однокругового — турнирную таблицу, шахматку результатов и, по желанию, таблицу положения команд по турам. Очки, разница мячей, места, карточки и снятия очков считаются автоматически по данным о матчах.
Турнир, в котором после первого этапа таблица делится на группы, оформляется несколькими таблицами: таблицы групп начинаются с итогов первого этапа — см. «Турнир из нескольких этапов».
Использование
Модуль вызывается напрямую из статьи:
{{#invoke:Football|main| … }}
Модуль читает только параметры самого вызова #invoke. Если нужен шаблон-обёртка, он должен явно передавать в модуль каждый параметр.
Модуль выводит по порядку:
- турнирную таблицу — без собственного заголовка (заголовок раздела ставится в статье);
- строку «Критерии определения мест» и, если с какой-то команды снимались очки, блок «Примечания»;
- раздел «Результаты» с шахматкой;
- раздел «Положение команд по турам» — только при
show_progress = yes.
Заголовки разделов — обычные вики-заголовки (по умолчанию === … ===), поэтому они попадают в оглавление статьи. Уровень заголовков меняется параметром heading_level.
Быстрый старт
{{#invoke:Турнирная таблица|main
| teams = СПА;ЦСК;ЗЕН
| name_СПА = Спартак
| name_ЦСК = ЦСКА
| name_ЗЕН = Зенит
| match1 = 1;1;СПА;ЦСК;2:1
| match2 = 2;1;ЦСК;ЗЕН;0:0
| match3 = 3;1;ЗЕН;СПА;1:3
| match4 = 4;2;ЦСК;СПА;
| match5 = 5;2;ЗЕН;ЦСК;
| match6 = 6;2;СПА;ЗЕН;
}}
Несыгранные матчи можно внести заранее с пустым счётом: в шахматке они видны как «—» и начинают учитываться, как только вписан счёт.
Для однокругового турнира добавьте rounds = 1 — см. «Шахматка».
Команды
| Параметр | Описание |
|---|---|
teams |
Обязательный. Коды команд через «;», например СПА;ЦСК;ЗЕН. Порядок кодов — это порядок строк и столбцов шахматки и строк таблицы по турам; итоговая таблица сортируется сама. Коды должны быть уникальными. Удобнее всего короткие коды из букв и цифр без пробелов: они входят в имена других параметров.
|
name_КОД |
Название команды, например name_СПА = Спартак. Можно использовать вики-разметку: name_СПА = [[ФК «Спартак»|Спартак]]. Если название не указано, выводится код.
|
Матчи
Каждый матч — отдельный параметр: match1, match2 и так далее. Нумерация без пропусков: модуль читает матчи по порядку и останавливается на первом отсутствующем или пустом номере, поэтому матчи после пропуска не учитываются.
Части значения разделяются «;». Допускаются три формата:
| Формат | Пример | Примечание |
|---|---|---|
тур;круг;хозяева;гости;счёт |
5;2;СПА;ЛОК;2:2 |
Полный формат. Номер тура нужен для таблицы положения по турам, номер круга — для режима шахматки resultsMode = round.
|
круг;хозяева;гости;счёт |
2;СПА;ЛОК;2:2 |
Матч не попадает в таблицу положения по турам. |
хозяева;гости;счёт |
СПА;ЛОК;2:2 |
Матч не попадает в таблицу положения по турам. |
Хозяева и гости указываются кодами из teams. Тур — число, круг — 1 или 2.
Возможные значения счёта:
| Значение | Что означает | Что видно в шахматке |
|---|---|---|
2:1 (можно 2 : 1) |
Матч сыгран: голы хозяев, двоеточие, голы гостей | Счёт и цвет результата |
пусто, - или — |
Матч ещё не сыгран | — |
перенесён или перенесен |
Матч перенесён | перенесён |
отменён или отменен |
Матч отменён | отменён |
| что-либо другое | Счёт не распознан, матч не учитывается | ошибка: … |
Техническое поражение записывается обычным счётом, например 0:3. Если за матч с команды ещё и сняли очки, это указывается отдельно — см. раздел «Снятие очков».
Очки за результат
| Параметр | По умолчанию | Описание |
|---|---|---|
points_win |
3 | Очки за победу |
points_draw |
1 | Очки за ничью |
points_loss |
0 | Очки за поражение |
Критерии определения мест
Параметры tie1 … tie10 задают критерии в порядке применения: если по первому критерию команды равны, сравнивается второй, и так далее. Если не указан ни один, используется порядок очки → разница → забитые → победы → название. Если команды равны по всем критериям, выше та, чей код раньше по алфавиту.
| Значение | Выше команда, у которой… |
|---|---|
очки |
больше очков (с учётом снятых и стартовых) |
разница |
лучше разница забитых и пропущенных мячей |
забитые |
больше забитых мячей |
победы |
больше побед |
игры |
больше сыгранных матчей |
личные |
лучше результаты в матчах между всеми командами, набравшими столько же очков: сравниваются очки в этих матчах, затем разница мячей в них, забитые в них и победы в них. Учитываются матчи этой таблицы и матчи h2h_matchN (см. «Личные встречи с учётом первого этапа»); снятые и стартовые очки здесь не учитываются. Критерий имеет смысл только после очки.
|
название |
название раньше по алфавиту (сравнивается текст параметра name_КОД как есть, вместе с разметкой)
|
Стартовые показатели команд (start_КОД) входят во все критерии, кроме личные.
Под таблицей выводится строка «Критерии определения мест: …» с выбранным порядком. Нераспознанные значения модуль пропускает, но показывает в этой строке, так что по ней удобно проверять написание.
Пример: очки, затем личные встречи, затем общая разница мячей.
| tie1 = очки | tie2 = личные | tie3 = разница
Цветовые зоны
Параметры highlight1 … highlight10 (без пропусков в нумерации) выделяют группы мест:
highlightN = место;количество;класс;текст
| Часть | Описание |
|---|---|
| место | Первое место зоны — в нумерации таблицы, то есть с учётом first_place (см. «Нумерация мест»)
|
| количество | Сколько мест подряд входит в зону (по умолчанию 1) |
| класс | CSS-класс. Строки зоны получают классы highlight и указанный, например class="highlight green". Сами цвета задаются стилями вики (см. «Оформление»).
|
| текст | Подпись зоны (необязательно). Может содержать ссылки и «;». |
Если хотя бы у одной зоны есть текст, в таблице появляется столбец «Повышение или выбывание»: подпись выводится одной объединённой ячейкой на всю зону, места между зонами — пустыми объединёнными ячейками. Зоны не должны пересекаться. Зона, которая начинается выше первого места таблицы, обрезается, а зоны целиком за пределами таблицы не выводятся.
| highlight1 = 1;1;green;Выход в Лигу чемпионов | highlight2 = 2;2;blue;Выход в Лигу конференций | highlight3 = 15;2;red;Выбывание в Первую лигу
Шахматка
Раздел «Результаты». Строки и столбцы — команды в порядке teams, в ячейке — матч команды строки с командой столбца.
| Параметр | Значения | Описание |
|---|---|---|
rounds |
2 (по умолчанию) или 1 |
Число кругов: 1 — однокруговой турнир
|
resultsMode |
round |
Только для двухкругового турнира: строки по кругам, а не «дома — в гостях» |
Двухкруговой турнир (по умолчанию): каждая пара играет по разу на поле каждой из команд, поэтому у команды две строки. В первой — её домашние матчи, во второй — выездные. При resultsMode = round первая строка показывает матчи первого круга, вторая — второго (по номеру круга в matchN).
Однокруговой турнир (rounds = 1): каждая пара играет один раз, поэтому у команды одна строка, а в ячейке — единственный матч пары, на чьём бы поле он ни был. Параметр resultsMode при этом не действует.
Счёт всегда записан как «хозяева:гости», а цвет ячейки показывает результат для команды строки: класс ui green — победа, ui yellow — ничья, ui red — поражение. Например, в однокруговой шахматке матч «Спартак» — ЦСКА 0:2 и в строке «Спартака», и в строке ЦСКА записан как «0:2», но у «Спартака» ячейка красная, а у ЦСКА — зелёная.
Если у пары больше матчей, чем ячеек (например, отменённый матч и переигровка), в ячейке показывается первый сыгранный из них, а если сыгранных нет — первый с пометкой «перенесён», «отменён» или «ошибка: …».
Положение команд по турам
Параметр show_progress = yes добавляет раздел «Положение команд по турам»: для каждого тура — место команды после всех матчей этого и предыдущих туров. Учитываются только матчи с номером тура (полный формат matchN); туры идут по возрастанию, строки — в порядке teams. Места определяются по тем же критериям, что и в итоговой таблице (личные встречи — только по матчам до этого тура включительно). Если ни у одного матча нет номера тура, вместо таблицы выводится пояснение.
Ячейки с определёнными местами можно выделить параметрами progress_highlight1 … progress_highlight10:
progress_highlightN = место;класс — ячейка получает классы highlight и указанный.
| show_progress = yes | progress_highlight1 = 1;green | progress_highlight2 = 16;red
Снятие очков учитывается начиная с тура матча, к которому оно привязано, а ячейка этого тура получает ту же сноску, что и команда в итоговой таблице.
Стартовые показатели (start_КОД) действуют с первого тура таблицы, а матчи h2h_matchN учитываются в критерии личные как сыгранные до первого тура. Места выводятся в нумерации таблицы (с учётом first_place), и в progress_highlightN места указываются в этой же нумерации.
Карточки
Карточки необязательны. Если они не указаны ни в одном матче, ни у одной команды и ни в одних стартовых показателях, столбцов карточек в таблице нет. Если они указаны хотя бы где-нибудь, после столбца «О» появляются три столбца — у всех команд.
Карточки учитываются раздельно, как в подробной футбольной статистике, чтобы вторая жёлтая карточка не засчитывалась дважды:
| Столбец | Вид | Что считается |
|---|---|---|
| | yellow |
Жёлтые карточки — все предупреждения, кроме второй жёлтой, после которой игрок удалён |
| | yellowred |
Удаления за вторую жёлтую карточку — по одному на каждое такое удаление |
| | red |
Прямые красные карточки |
Как записывать разные случаи (один игрок в одном матче):
| Что произошло | yellow | yellowred | red |
|---|---|---|---|
| Одна жёлтая карточка | 1 | 0 | 0 |
| Две жёлтые карточки и удаление | 1 | 1 | 0 |
| Прямая красная карточка | 0 | 0 | 1 |
| Жёлтая, затем прямая красная | 1 | 0 | 1 |
Общее число удалений команды равно сумме столбцов yellowred и red.
Карточки выводятся для справки и на места в таблице не влияют.
Карточки по матчам
matchN_yellow, matchN_yellowred, matchN_red — карточки в матче N в формате хозяева:гости:
| match1 = 1;1;СПА;ЦСК;2:1 | match1_yellow = 2:3 | match1_yellowred = 0:1
Здесь у хозяев две жёлтые карточки, у гостей — три жёлтые и одно удаление за вторую жёлтую. Неуказанный вид карточек считается как 0:0. Карточки суммируются по всем матчам, где они указаны, независимо от счёта, и прибавляются к карточкам из стартовых показателей (start_КОД), если они там заданы.
Итог команды
yellow_КОД, yellowred_КОД, red_КОД — итоговое количество карточек команды (целое неотрицательное число). Если у команды указан хотя бы один из этих параметров, все её карточки берутся только из них (неуказанные считаются нулём), а карточки из матчей и стартовых показателей этой команды не учитываются. У остальных команд карточки по-прежнему считаются по матчам.
| yellow_ЗЕН = 41 | yellowred_ЗЕН = 2 | red_ЗЕН = 1
Так удобно поступать, если поматчевой статистики нет или итог нужно привести в соответствие с официальными данными.
Снятие очков
Снятие очков привязывается к конкретному матчу:
matchN_deduction = код;очки;причина
| Часть | Описание |
|---|---|
| код | Команда, с которой сняты очки. Должна быть одной из двух команд матча N. |
| очки | Сколько очков снято: положительное число, можно дробное (1,5).
|
| причина | Необязательно. Пишется как продолжение фразы: «за неявку на матч», «по решению КДК». Точка в конце не нужна. Можно использовать ссылки и «;». |
Для второго снятия в том же матче (например, с обеих команд) используется matchN_deduction2, и так далее до matchN_deduction5.
| match7 = 4;2;ЦСК;СПА;1:1 | match7_deduction = СПА;3;за участие в матче дисквалифицированного игрока
Результат:
- в итоговой таблице очки команды уменьшены, у названия стоит сноска вида [a], а при наведении на очки видна подсказка «Снято 3 очка»;
- под таблицей появляется блок «Примечания» с текстом С команды «Спартак» снято 3 очка в 4-м туре (матч «ЦСКА» — «Спартак») за участие в матче дисквалифицированного игрока. Номер тура упоминается, если он указан в матче;
- в таблице положения по турам очки снимаются начиная с этого тура, а в ячейке этого тура стоит та же сноска.
Сноски обозначаются буквами a, b, c… сверху вниз по итоговой таблице. Снятие действует независимо от того, сыгран ли матч, и не влияет на критерий личные.
Очки, снятые с команды до начала таблицы (например, в первом этапе турнира), здесь не указываются: они уже входят в её стартовые очки — см. «Стартовые показатели».
Турнир из нескольких этапов
Бывает, что после первого этапа таблица делится на группы — пополам или в другой пропорции, например 6 первых команд играют за 1–6 места, а 8 последних — за 7–14 места, — и команды доигрывают в своих группах. Такой турнир оформляется несколькими таблицами:
- первый этап — обычная таблица всех команд (если этап однокруговой — с
rounds = 1); - каждая группа второго этапа — отдельная таблица только из команд группы и матчей второго этапа. Итоги первого этапа задаются стартовыми показателями
start_КОД, а у групп ниже первой нумерация мест начинается сfirst_place. Если среди критериев естьличные, в таблицу группы добавляются ещё и матчи первого этапа —h2h_matchN.
Стартовые показатели переписываются из итоговой таблицы первого этапа вручную. Зато так можно описать любой регламент перехода: сохраняются все результаты, очки делятся пополам или учитываются только матчи с командами своей группы.
Полный пример — в разделе «Пример турнира из двух этапов».
Стартовые показатели
start_КОД = И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК — показатели, с которыми команда начинает таблицу. Части идут в том же порядке, что и столбцы итоговой таблицы (без РМ — она вычисляется сама), а за ними карточки:
| Часть | Что означает | Если пусто |
|---|---|---|
| И | Игры | В + Н + П |
| В, Н, П | Победы, ничьи, поражения | 0 |
| МЗ, МП | Забитые и пропущенные мячи | 0 |
| О | Очки; могут быть дробными (12,5) и отрицательными |
Вычисляются по победам, ничьим и поражениям с учётом points_win, points_draw, points_loss
|
| ЖК, 2ЖК, КК | Жёлтые карточки, удаления за вторую жёлтую карточку, прямые красные — как yellow, yellowred, red в разделе «Карточки» |
0 |
Все части, кроме О, — целые неотрицательные числа. Если И указано, оно должно быть равно В + Н + П. Последние части можно не писать: при 3 очках за победу start_СПА = 13;9;2;2;27;10 означает то же, что start_СПА = 13;9;2;2;27;10;29.
Стартовые показатели:
- прибавляются к результатам матчей таблицы и учитываются во всех критериях, кроме
личные; - в таблице положения по турам действуют с первого тура этой таблицы;
- в шахматке не видны: в ней только матчи этой таблицы;
- если в них указаны карточки, те складываются с карточками из матчей, а итог команды (
yellow_КОДи т. д.) заменяет и те и другие.
Очки, снятые в первом этапе, уже входят в очки О, и сноски о них в таблице группы не выводятся. Снятие очков за матчи второго этапа указывается как обычно — matchN_deduction.
Если во второй этап переходят не все результаты первого, в start_КОД указывается только то, что переходит. Например, если команды сохраняют половину очков, а игры и мячи считаются заново: start_СПА = ;;;;;;14,5.
Параметры start_КОД для кодов, которых нет в teams, не читаются, поэтому в таблицы всех групп можно вставить один и тот же блок со всеми командами.
Нумерация мест
first_place — номер первого места таблицы, по умолчанию 1. Для группы за 7–14 места указывается first_place = 7: места в турнирной таблице и в таблице положения по турам нумеруются с 7-го. В этой же нумерации указываются места в highlightN и progress_highlightN, например highlight1 = 13;2;red;Выбывание.
Зона, которая начинается выше первого места таблицы, обрезается, а зоны целиком за пределами таблицы не выводятся, поэтому одинаковый набор зон тоже можно вставить в таблицы всех групп.
Личные встречи с учётом первого этапа
В таблице группы критерий личные видит только её матчи, а по регламенту обычно учитываются все матчи сезона между командами. Матчи первого этапа добавляются параметрами h2h_match1, h2h_match2… (без пропусков в нумерации) в тех же форматах, что и matchN. Такие матчи:
- учитываются только в критерии
личные, а в таблице положения по турам — как сыгранные до первого тура; - не влияют на показатели команд, шахматку и таблицу по турам: итоги первого этапа уже есть в
start_КОД; - пропускаются, если в матче играет команда, которой нет в
teams, или если матч не сыгран.
Проще всего скопировать в таблицу группы строки matchN из таблицы первого этапа и заменить в них match на h2h_match. Матчи с командами других групп модуль пропустит сам, а получившиеся из карточек и снятий очков строки вроде h2h_match5_yellow не читает — их можно удалить.
Оформление
| Параметр | Описание |
|---|---|
heading_level |
Уровень заголовков «Результаты» и «Положение команд по турам»: от 2 до 6, по умолчанию 3 (=== Результаты ===). При значении 2 — == Результаты ==.
|
id |
Префикс якорей для сносок. По умолчанию вычисляется по списку команд, поэтому обычно его указывать не нужно. Задайте разные id, если на одной странице две таблицы с одинаковым списком команд и со снятием очков. Лучше использовать латиницу, цифры и дефис.
|
CSS-классы, которые выводит модуль:
| Класс | Где используется |
|---|---|
football-tables |
Общий блок вокруг всех таблиц |
football-standings, football-results, football-progress |
Итоговая таблица, шахматка, таблица по турам |
football-info |
Строка критериев и блок примечаний |
football-notes |
Блок примечаний |
football-note-ref |
Сноска о снятии очков |
football-deduction |
Ячейка таблицы по турам, в которой сняты очки |
football-cards |
Ячейки столбцов карточек |
football-card, football-card-yellow, football-card-yellowred, football-card-red |
Значки карточек в заголовке |
highlight и класс зоны |
Строки цветовых зон и выделенные ячейки таблицы по турам |
ui green, ui yellow, ui red |
Результаты в шахматке |
football-corner, football-team-header, football-diagonal |
Угловая ячейка, названия команд и диагональ шахматки |
Цвета зон и результатов модуль не задаёт — они берутся из стилей вики. Если таких стилей ещё нет, их можно добавить в MediaWiki:Common.css, например:
.football-tables .highlight.green { background-color: #bbf3bb; }
.football-tables .highlight.blue { background-color: #bbebff; }
.football-tables .highlight.red { background-color: #ffbbbb; }
.football-results td.ui.green { background-color: #bbf3bb; }
.football-results td.ui.yellow { background-color: #ffffbb; }
.football-results td.ui.red { background-color: #ffbbbb; }
.football-progress td.football-deduction { font-weight: bold; }
Сообщения об ошибках
При ошибке во входных данных модуль вместо таблиц выводит сообщение, начинающееся со слова «Ошибка»:
| Сообщение | Что исправить |
|---|---|
| не указан параметр teams | Добавьте список команд. |
| команда с кодом … указана более одного раза | Уберите повтор из teams.
|
| матч N имеет неправильный формат | В matchN меньше трёх частей через «;».
|
| параметр h2h_matchN имеет неправильный формат | В h2h_matchN меньше трёх частей через «;».
|
| в матче N неизвестная команда … | Код не входит в teams. Проверьте написание, в том числе не попали ли латинские буквы вместо кириллических.
|
| параметр matchN_yellow имеет неправильный формат … | Карточки в матче записываются как число:число (то же для matchN_yellowred и matchN_red).
|
| в параметре matchN_deduction должна быть указана команда матча … | Код команды должен совпадать с хозяевами или гостями матча N. |
| в параметре matchN_deduction не указано, сколько очков снято | Вторая часть должна быть ненулевым числом. |
| параметр yellow_КОД должен быть целым неотрицательным числом … | Исправьте итог карточек команды (то же для yellowred_КОД и red_КОД).
|
| параметр rounds может быть равен 1 … или 2 … | Укажите 1 для однокругового турнира или уберите параметр.
|
| параметр first_place должен быть целым положительным числом … | Укажите номер первого места таблицы, например 7.
|
| в параметре start_КОД больше 10 значений … | Частей не больше десяти: И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК.
|
| в параметре start_КОД значение … должно быть целым неотрицательным числом … | Исправьте названную часть. Дробными и отрицательными могут быть только очки. |
| в параметре start_КОД очки (О) должны быть числом … | Седьмая часть — число, например 29 или 14,5.
|
| в параметре start_КОД число игр … не равно сумме побед, ничьих и поражений … | Проверьте И, В, Н и П или оставьте И пустым — тогда оно вычислится. |
Нераспознанный счёт работу модуля не останавливает: такой матч не учитывается, а в шахматке на его месте видно «ошибка: …».
Пример
Четыре команды, шесть туров, карточки, снятие очков, цветовые зоны и таблица положения по турам. Без снятия очков первое место занял бы «Спартак».
{{#invoke:Турнирная таблица|main
| teams = СПА;ЦСК;ЗЕН;ЛОК
| name_СПА = Спартак
| name_ЦСК = ЦСКА
| name_ЗЕН = Зенит
| name_ЛОК = Локомотив
| match1 = 1;1;СПА;ЦСК;2:1
| match2 = 1;1;ЗЕН;ЛОК;1:1
| match3 = 2;1;ЦСК;ЗЕН;0:2
| match4 = 2;1;ЛОК;СПА;1:3
| match5 = 3;1;СПА;ЗЕН;1:1
| match6 = 3;1;ЦСК;ЛОК;2:0
| match7 = 4;2;ЦСК;СПА;1:1
| match8 = 4;2;ЛОК;ЗЕН;0:1
| match9 = 5;2;ЗЕН;ЦСК;3:1
| match10 = 5;2;СПА;ЛОК;2:2
| match11 = 6;2;ЗЕН;СПА;0:2
| match12 = 6;2;ЛОК;ЦСК;перенесён
| match1_yellow = 2:3
| match1_yellowred = 0:1
| match4_yellow = 3:1
| match7_yellow = 1:2
| match7_red = 1:0
| match9_yellow = 2:2
| match11_yellow = 1:3
| match11_yellowred = 1:0
| match7_deduction = СПА;3;за участие в матче дисквалифицированного игрока
| highlight1 = 1;1;green;Выход в финальный турнир
| highlight2 = 4;1;red;Выбывание
| show_progress = yes
| progress_highlight1 = 1;green
| progress_highlight2 = 4;red
}}
Результат:
| Место | Команда | И | В | Н | П | МЗ | МП | РМ | О | | | | Повышение или выбывание |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | Зенит | 6 | 3 | 2 | 1 | 8 | 5 | +3 | 11 | 3 | 1 | 0 | Выход в финальный турнир |
| 2 | Спартак[a] | 6 | 3 | 3 | 0 | 11 | 6 | +5 | 9 | 8 | 0 | 0 | |
| 3 | ЦСКА | 5 | 1 | 1 | 3 | 5 | 8 | -3 | 4 | 6 | 1 | 1 | |
| 4 | Локомотив | 5 | 0 | 2 | 3 | 4 | 9 | -5 | 2 | 3 | 0 | 0 | Выбывание |
- ↑ С команды «Спартак» снято 3 очка в 4-м туре (матч «ЦСКА» — «Спартак») за участие в матче дисквалифицированного игрока.
Результаты
| Спартак | ЦСКА | Зенит | Локомотив | |
|---|---|---|---|---|
| Спартак | — | 2:1 | 1:1 | 2:2 |
| 1:1 | 0:2 | 1:3 | ||
| ЦСКА | 1:1 | — | 0:2 | 2:0 |
| 2:1 | 3:1 | перенесён | ||
| Зенит | 0:2 | 3:1 | — | 1:1 |
| 1:1 | 0:2 | 0:1 | ||
| Локомотив | 1:3 | перенесён | 0:1 | — |
| 2:2 | 2:0 | 1:1 |
Положение команд по турам
| Команда | Тур | |||||
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| Спартак | 1 | 1 | 1 | 2[a] | 2 | 2 |
| ЦСКА | 4 | 4 | 3 | 3 | 3 | 3 |
| Зенит | 2 | 2 | 2 | 1 | 1 | 1 |
| Локомотив | 3 | 3 | 4 | 4 | 4 | 4 |
Пример турнира из двух этапов
Шесть команд играют первый этап в один круг, затем три лучшие разыгрывают 1–3 места, а остальные — 4–6 места: в своих группах команды играют в два круга. Все результаты первого этапа сохраняются, а при равенстве очков места определяются по личным встречам за весь сезон.
Стартовые показатели в таблицах групп — это строки итоговой таблицы первого этапа, а в h2h_matchN — матчи первого этапа между командами группы. В группе за 4–6 места «Динамо» и ЦСКА набрали поровну очков и во втором этапе обменялись гостевыми победами 2:1, поэтому «Динамо» выше благодаря победе 2:0 в первом этапе. Без строк h2h_match выше оказался бы ЦСКА — у него лучше разница мячей.
=== Первый этап ===
{{#invoke:Турнирная таблица|main
| teams = СПА;ЦСК;ЗЕН;ЛОК;ДИН;КРА
| name_СПА = Спартак
| name_ЦСК = ЦСКА
| name_ЗЕН = Зенит
| name_ЛОК = Локомотив
| name_ДИН = Динамо
| name_КРА = Краснодар
| rounds = 1
| heading_level = 4
| tie1 = очки
| tie2 = личные
| tie3 = разница
| tie4 = забитые
| match1 = 1;1;СПА;КРА;1:1
| match2 = 1;1;ДИН;ЦСК;2:0
| match3 = 1;1;ЗЕН;ЛОК;1:2
| match4 = 2;1;ДИН;СПА;0:0
| match5 = 2;1;КРА;ЛОК;1:0
| match6 = 2;1;ЗЕН;ЦСК;1:1
| match7 = 3;1;СПА;ЛОК;0:1
| match8 = 3;1;ЗЕН;ДИН;3:1
| match9 = 3;1;КРА;ЦСК;2:1
| match10 = 4;1;ЗЕН;СПА;1:3
| match11 = 4;1;ЛОК;ЦСК;2:0
| match12 = 4;1;КРА;ДИН;3:0
| match13 = 5;1;СПА;ЦСК;0:2
| match14 = 5;1;КРА;ЗЕН;1:2
| match15 = 5;1;ЛОК;ДИН;2:1
| highlight1 = 1;3;green;Группа за 1–3 места
| highlight2 = 4;3;blue;Группа за 4–6 места
}}
=== Группа за 1–3 места ===
{{#invoke:Турнирная таблица|main
| teams = ЛОК;КРА;ЗЕН
| name_ЛОК = Локомотив
| name_КРА = Краснодар
| name_ЗЕН = Зенит
| heading_level = 4
| tie1 = очки
| tie2 = личные
| tie3 = разница
| tie4 = забитые
| start_ЛОК = 5;4;0;1;7;3;12
| start_КРА = 5;3;1;1;8;4;10
| start_ЗЕН = 5;2;1;2;8;8;7
| h2h_match1 = 1;1;ЗЕН;ЛОК;1:2
| h2h_match2 = 2;1;КРА;ЛОК;1:0
| h2h_match3 = 5;1;КРА;ЗЕН;1:2
| match1 = 6;1;ЗЕН;КРА;2:2
| match2 = 7;1;ЗЕН;ЛОК;0:2
| match3 = 8;1;ЛОК;КРА;1:0
| match4 = 9;2;КРА;ЗЕН;0:1
| match5 = 10;2;ЛОК;ЗЕН;2:0
| match6 = 11;2;КРА;ЛОК;0:1
| highlight1 = 1;1;green;Чемпион
| show_progress = yes
| progress_highlight1 = 1;green
}}
=== Группа за 4–6 места ===
{{#invoke:Турнирная таблица|main
| teams = СПА;ДИН;ЦСК
| name_СПА = Спартак
| name_ДИН = Динамо
| name_ЦСК = ЦСКА
| first_place = 4
| heading_level = 4
| tie1 = очки
| tie2 = личные
| tie3 = разница
| tie4 = забитые
| start_СПА = 5;1;2;2;4;5;5
| start_ДИН = 5;1;1;3;4;8;4
| start_ЦСК = 5;1;1;3;4;7;4
| h2h_match1 = 1;1;ДИН;ЦСК;2:0
| h2h_match2 = 2;1;ДИН;СПА;0:0
| h2h_match3 = 5;1;СПА;ЦСК;0:2
| match1 = 6;1;ЦСК;ДИН;1:2
| match2 = 7;1;ЦСК;СПА;0:1
| match3 = 8;1;СПА;ДИН;1:2
| match4 = 9;2;ДИН;ЦСК;1:2
| match5 = 10;2;СПА;ЦСК;0:2
| match6 = 11;2;ДИН;СПА;0:1
| highlight1 = 6;1;red;Выбывание
| show_progress = yes
| progress_highlight1 = 6;red
}}
Результат:
Первый этап
| Место | Команда | И | В | Н | П | МЗ | МП | РМ | О | Повышение или выбывание |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | Локомотив | 5 | 4 | 0 | 1 | 7 | 3 | +4 | 12 | Группа за 1–3 места |
| 2 | Краснодар | 5 | 3 | 1 | 1 | 8 | 4 | +4 | 10 | |
| 3 | Зенит | 5 | 2 | 1 | 2 | 8 | 8 | 0 | 7 | |
| 4 | Спартак | 5 | 1 | 2 | 2 | 4 | 5 | -1 | 5 | Группа за 4–6 места |
| 5 | Динамо | 5 | 1 | 1 | 3 | 4 | 8 | -4 | 4 | |
| 6 | ЦСКА | 5 | 1 | 1 | 3 | 4 | 7 | -3 | 4 |
Результаты
| Спартак | ЦСКА | Зенит | Локомотив | Динамо | Краснодар | |
|---|---|---|---|---|---|---|
| Спартак | — | 0:2 | 1:3 | 0:1 | 0:0 | 1:1 |
| ЦСКА | 0:2 | — | 1:1 | 2:0 | 2:0 | 2:1 |
| Зенит | 1:3 | 1:1 | — | 1:2 | 3:1 | 1:2 |
| Локомотив | 0:1 | 2:0 | 1:2 | — | 2:1 | 1:0 |
| Динамо | 0:0 | 2:0 | 3:1 | 2:1 | — | 3:0 |
| Краснодар | 1:1 | 2:1 | 1:2 | 1:0 | 3:0 | — |
Группа за 1–3 места
| Место | Команда | И | В | Н | П | МЗ | МП | РМ | О | Повышение или выбывание |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | Локомотив | 9 | 8 | 0 | 1 | 13 | 3 | +10 | 24 | Чемпион |
| 2 | Зенит | 9 | 3 | 2 | 4 | 11 | 14 | -3 | 11 | |
| 3 | Краснодар | 9 | 3 | 2 | 4 | 10 | 9 | +1 | 11 |
Результаты
| Локомотив | Краснодар | Зенит | |
|---|---|---|---|
| Локомотив | — | 1:0 | 2:0 |
| 0:1 | 0:2 | ||
| Краснодар | 0:1 | — | 0:1 |
| 1:0 | 2:2 | ||
| Зенит | 0:2 | 2:2 | — |
| 2:0 | 0:1 |
Положение команд по турам
| Команда | Тур | |||||
|---|---|---|---|---|---|---|
| 6 | 7 | 8 | 9 | 10 | 11 | |
| Локомотив | 1 | 1 | 1 | 1 | 1 | 1 |
| Краснодар | 2 | 2 | 2 | 3 | 3 | 3 |
| Зенит | 3 | 3 | 3 | 2 | 2 | 2 |
Группа за 4–6 места
| Место | Команда | И | В | Н | П | МЗ | МП | РМ | О | Повышение или выбывание |
|---|---|---|---|---|---|---|---|---|---|---|
| 4 | Спартак | 9 | 3 | 2 | 4 | 7 | 9 | -2 | 11 | |
| 5 | Динамо | 9 | 3 | 1 | 5 | 9 | 13 | -4 | 10 | |
| 6 | ЦСКА | 9 | 3 | 1 | 5 | 9 | 11 | -2 | 10 | Выбывание |
Результаты
| Спартак | Динамо | ЦСКА | |
|---|---|---|---|
| Спартак | — | 1:2 | 0:2 |
| 0:1 | 0:1 | ||
| Динамо | 0:1 | — | 1:2 |
| 1:2 | 1:2 | ||
| ЦСКА | 0:1 | 1:2 | — |
| 0:2 | 1:2 |
Положение команд по турам
| Команда | Тур | |||||
|---|---|---|---|---|---|---|
| 6 | 7 | 8 | 9 | 10 | 11 | |
| Спартак | 5 | 4 | 5 | 5 | 6 | 4 |
| Динамо | 4 | 5 | 4 | 4 | 4 | 5 |
| ЦСКА | 6 | 6 | 6 | 6 | 5 | 6 |
local p = {}
-- =========================================================
-- НАСТРОЙКИ
-- =========================================================
-- Виды карточек в порядке вывода столбцов.
-- key — часть имени параметров: matchN_<key> и <key>_КОД.
local CARD_TYPES = {
{
key = "yellow",
title = "Жёлтые карточки",
background = "#ffd200"
},
{
key = "yellowred",
title = "Удаления за вторую жёлтую карточку",
background = "linear-gradient(135deg, #ffd200 50%, #e30613 50%)"
},
{
key = "red",
title = "Прямые красные карточки",
background = "#e30613"
}
}
-- Сколько снятий очков можно привязать к одному матчу:
-- matchN_deduction, matchN_deduction2 … matchN_deduction5
local MAX_DEDUCTIONS = 5
-- Части параметра start_КОД (стартовые показатели команды)
-- в порядке записи. label — обозначение части
-- в описании формата и в сообщениях об ошибках.
local START_PARTS = {
{ key = "played", label = "И" },
{ key = "wins", label = "В" },
{ key = "draws", label = "Н" },
{ key = "losses", label = "П" },
{ key = "goalsFor", label = "МЗ" },
{ key = "goalsAgainst", label = "МП" },
{ key = "points", label = "О" },
{ key = "yellow", label = "ЖК", card = true },
{ key = "yellowred", label = "2ЖК", card = true },
{ key = "red", label = "КК", card = true }
}
-- Формат параметра start_КОД: «И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК»
local START_FORMAT
do
local labels = {}
for _, part in ipairs(START_PARTS) do
table.insert(labels, part.label)
end
START_FORMAT = table.concat(labels, ";")
end
-- =========================================================
-- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
-- =========================================================
local function trim(value)
if value == nil then
return ""
end
return mw.text.trim(tostring(value))
end
local function split(value, separator)
local result = {}
for part in mw.text.gsplit(value, separator, true) do
table.insert(result, trim(part))
end
return result
end
local function number(value, default)
local n = tonumber(value)
if n == nil then
return default
end
return n
end
-- Число для текста: 3 → «3», 1.5 → «1,5»
local function formatNumber(n)
if n == math.floor(n) then
return string.format("%d", n)
end
return (string.gsub(tostring(n), "%.", ","))
end
-- Форма слова после числа: 1 очко, 2 очка, 5 очков
local function plural(n, one, few, many)
n = math.abs(n)
if n ~= math.floor(n) then
return few
end
local lastTwo = n % 100
local last = n % 10
if lastTwo >= 11 and lastTwo <= 14 then
return many
elseif last == 1 then
return one
elseif last >= 2 and last <= 4 then
return few
end
return many
end
-- Пара значений «хозяева:гости», например «2:1»
local function parsePair(value)
local home, away =
trim(value):match("^(%d+)%s*:%s*(%d+)$")
if home and away then
return tonumber(home), tonumber(away)
end
return nil
end
-- Метка сноски: 1 → a, 2 → b … 26 → z, 27 → aa
local function noteLabel(index)
local label = ""
while index > 0 do
local rest = (index - 1) % 26
label = string.char(97 + rest) .. label
index = math.floor((index - 1) / 26)
end
return label
end
-- =========================================================
-- РАЗБОР СЧЁТА
-- =========================================================
local function parseScore(score)
score = trim(score)
-- Матч не сыгран
if score == "" or score == "-" or score == "—" then
return {
played = false,
status = ""
}
end
-- Перенесён
if score == "перенесён" or score == "перенесен" then
return {
played = false,
status = "перенесён"
}
end
-- Отменён
if score == "отменён" or score == "отменен" then
return {
played = false,
status = "отменён"
}
end
-- Обычный счёт
local home, away = score:match("^(%d+)%s*:%s*(%d+)$")
if home and away then
return {
played = true,
homeGoals = tonumber(home),
awayGoals = tonumber(away),
score = home .. ":" .. away
}
end
-- Неизвестный формат
return {
played = false,
status = "ошибка: " .. score
}
end
-- =========================================================
-- РАЗБОР ЗАПИСИ О МАТЧЕ
-- =========================================================
-- Значение matchN или h2h_matchN в одном из форматов:
-- тур;круг;хозяева;гости;счёт
-- круг;хозяева;гости;счёт
-- хозяева;гости;счёт
-- Возвращает nil, если частей меньше трёх.
local function parseMatchValue(value)
local parts = split(value, ";")
-- Формат с номером тура
if #parts >= 5 then
return {
tour = parts[1],
round = parts[2],
home = parts[3],
away = parts[4],
score = parts[5]
}
end
-- Формат с номером круга
if #parts == 4 then
return {
tour = "",
round = parts[1],
home = parts[2],
away = parts[3],
score = parts[4]
}
end
-- Формат без номера круга
if #parts == 3 then
return {
tour = "",
round = "",
home = parts[1],
away = parts[2],
score = parts[3]
}
end
return nil
end
-- =========================================================
-- СОЗДАНИЕ КОМАНДЫ
-- =========================================================
local function newTeam(code, name)
return {
code = code,
name = name,
played = 0,
wins = 0,
draws = 0,
losses = 0,
goalsFor = 0,
goalsAgainst = 0,
points = 0,
-- Сколько очков снято всего
deducted = 0,
-- Сноски о снятии очков
notes = {},
-- Карточки из стартовых показателей и матчей
matchCards = {
yellow = 0,
yellowred = 0,
red = 0
},
-- Итоговые карточки (из матчей или из параметров команды)
yellow = 0,
yellowred = 0,
red = 0
}
end
-- Добавляет к показателям команды стартовые
-- (параметр start_КОД), если они указаны
local function addStart(team, start)
if not start then
return
end
team.played = team.played + start.played
team.wins = team.wins + start.wins
team.draws = team.draws + start.draws
team.losses = team.losses + start.losses
team.goalsFor =
team.goalsFor + start.goalsFor
team.goalsAgainst =
team.goalsAgainst + start.goalsAgainst
team.points =
team.points + start.points
for _, cardType in ipairs(CARD_TYPES) do
team.matchCards[cardType.key] =
team.matchCards[cardType.key] + start[cardType.key]
end
end
-- =========================================================
-- СОЗДАНИЕ HTML
-- =========================================================
local function cell(tag, content, class)
local element = mw.html.create(tag)
if class and class ~= "" then
element
:addClass(class)
end
if content ~= nil then
element
:wikitext(tostring(content))
end
return element
end
-- Значок карточки для заголовка столбца.
-- Рисуется средствами CSS, поэтому не нужны файлы изображений.
local function cardIcon(cardType)
return mw.html.create("span")
:addClass("football-card")
:addClass("football-card-" .. cardType.key)
:attr("title", cardType.title)
:css("display", "inline-block")
:css("width", "0.7em")
:css("height", "0.95em")
:css("vertical-align", "middle")
:css("border-radius", "0.12em")
:css("border", "1px solid rgba(0, 0, 0, 0.35)")
:css("background", cardType.background)
:wikitext("​")
end
-- =========================================================
-- ОСНОВНАЯ ФУНКЦИЯ
-- =========================================================
function p.main(frame)
local args = frame.args
-- -----------------------------------------------------
-- НАСТРОЙКИ ОЧКОВ
-- -----------------------------------------------------
local pointsWin =
number(args.points_win, 3)
local pointsDraw =
number(args.points_draw, 1)
local pointsLoss =
number(args.points_loss, 0)
-- -----------------------------------------------------
-- ЗАГОЛОВКИ РАЗДЕЛОВ
-- -----------------------------------------------------
-- 3 → «=== Заголовок ===» (по умолчанию),
-- 2 → «== Заголовок ==», 4 → «==== Заголовок ====»
local headingLevel =
math.floor(number(args.heading_level, 3))
headingLevel =
math.max(2, math.min(6, headingLevel))
local function heading(text)
local marks = string.rep("=", headingLevel)
-- Заголовок вики-разметки должен стоять
-- на отдельной строке
return "\n" .. marks .. " " .. text .. " " .. marks .. "\n"
end
-- -----------------------------------------------------
-- КОМАНДЫ
-- -----------------------------------------------------
local teamsParameter = trim(args.teams)
if teamsParameter == "" then
return "'''Ошибка:''' не указан параметр ''teams''."
end
local teamCodes = split(teamsParameter, ";")
local teams = {}
local teamOrder = {}
for _, code in ipairs(teamCodes) do
if code ~= "" then
if teams[code] then
return "'''Ошибка:''' команда с кодом '''" ..
code .. "''' указана более одного раза."
end
local name = trim(args["name_" .. code])
if name == "" then
name = code
end
teams[code] = newTeam(code, name)
table.insert(teamOrder, code)
end
end
-- -----------------------------------------------------
-- СТАРТОВЫЕ ПОКАЗАТЕЛИ
-- -----------------------------------------------------
-- start_КОД = И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК — показатели,
-- с которыми команда начинает турнир. Нужны, например,
-- когда после первого этапа таблица делится на группы:
-- таблица каждой группы начинается с итогов первого этапа.
-- Пустая часть считается нулём; пустые И и О вычисляются
-- по победам, ничьим и поражениям. Параметры для кодов,
-- которых нет в teams, не читаются.
-- Возвращает показатели (nil, если параметр не указан)
-- или nil и текст ошибки
local function parseStart(code)
local paramName = "start_" .. code
local raw = trim(args[paramName])
if raw == "" then
return nil
end
local function fail(text)
return nil,
"'''Ошибка:''' в параметре ''" ..
paramName .. "'' " .. text
end
local parts = split(raw, ";")
if #parts > #START_PARTS then
return fail(
"больше " .. #START_PARTS .. " значений. " ..
"Формат: ''" .. START_FORMAT .. "''."
)
end
local start = {
-- Указаны ли карточки
hasCards = false
}
for index, part in ipairs(START_PARTS) do
local value = parts[index] or ""
if part.key == "points" then
-- Очки могут быть дробными и отрицательными:
-- «12,5», «-3» (пустые вычисляются ниже)
if value ~= "" then
local normalized =
string.gsub(value, ",", ".")
normalized =
string.gsub(normalized, "−", "-")
local amount = tonumber(normalized)
if amount == nil or
amount ~= amount or
math.abs(amount) == math.huge then
return fail(
"очки (О) должны быть числом, " ..
"а указано «" .. value .. "»."
)
end
start.points = amount
end
elseif value == "" then
-- Пустое число игр вычисляется ниже
if part.key ~= "played" then
start[part.key] = 0
end
elseif value:match("^%d+$") then
start[part.key] = tonumber(value)
if part.card then
start.hasCards = true
end
else
return fail(
"значение " .. part.label ..
" должно быть целым неотрицательным числом, " ..
"а указано «" .. value .. "»."
)
end
end
local results =
start.wins + start.draws + start.losses
if start.played == nil then
start.played = results
elseif start.played ~= results then
return fail(
"число игр (" .. start.played .. ") не равно " ..
"сумме побед, ничьих и поражений (" ..
results .. ")."
)
end
if start.points == nil then
start.points =
start.wins * pointsWin +
start.draws * pointsDraw +
start.losses * pointsLoss
end
return start
end
for _, code in ipairs(teamOrder) do
local start, errorText = parseStart(code)
if errorText then
return errorText
end
if start then
teams[code].start = start
addStart(teams[code], start)
end
end
-- Префикс якорей для сносок. Он должен быть уникальным
-- на странице, поэтому по умолчанию зависит от списка команд.
local idPrefix = trim(args.id)
if idPrefix == "" then
idPrefix = "football"
if mw.hash and mw.hash.hashValue then
local ok, hash = pcall(
mw.hash.hashValue,
"crc32b",
teamsParameter
)
if ok and hash then
idPrefix = idPrefix .. "-" .. hash
end
end
else
-- Символы, которые ломают якорь или ссылку [[#…]]
idPrefix = string.gsub(idPrefix, "[%s%[%]{}|#<>\"'&]+", "_")
end
-- -----------------------------------------------------
-- УЧЁТ РЕЗУЛЬТАТА И СНЯТИЯ ОЧКОВ
-- -----------------------------------------------------
-- Добавляет результат сыгранного матча в статистику.
-- teamsMap — итоговые команды или команды «по турам».
local function addResult(teamsMap, match)
local homeTeam = teamsMap[match.home]
local awayTeam = teamsMap[match.away]
homeTeam.played = homeTeam.played + 1
awayTeam.played = awayTeam.played + 1
homeTeam.goalsFor =
homeTeam.goalsFor + match.homeGoals
homeTeam.goalsAgainst =
homeTeam.goalsAgainst + match.awayGoals
awayTeam.goalsFor =
awayTeam.goalsFor + match.awayGoals
awayTeam.goalsAgainst =
awayTeam.goalsAgainst + match.homeGoals
if match.homeGoals > match.awayGoals then
homeTeam.wins = homeTeam.wins + 1
awayTeam.losses = awayTeam.losses + 1
homeTeam.points =
homeTeam.points + pointsWin
awayTeam.points =
awayTeam.points + pointsLoss
elseif match.homeGoals < match.awayGoals then
awayTeam.wins = awayTeam.wins + 1
homeTeam.losses = homeTeam.losses + 1
awayTeam.points =
awayTeam.points + pointsWin
homeTeam.points =
homeTeam.points + pointsLoss
else
homeTeam.draws = homeTeam.draws + 1
awayTeam.draws = awayTeam.draws + 1
homeTeam.points =
homeTeam.points + pointsDraw
awayTeam.points =
awayTeam.points + pointsDraw
end
end
-- Снимает очки, привязанные к матчу
-- (независимо от того, сыгран ли он)
local function applyDeductions(teamsMap, match)
for _, deduction in ipairs(match.deductions) do
local team = teamsMap[deduction.team]
team.points =
team.points - deduction.points
team.deducted =
team.deducted + deduction.points
end
end
-- -----------------------------------------------------
-- МАТЧИ
-- -----------------------------------------------------
local matches = {}
-- Все снятия очков в порядке матчей
local deductions = {}
-- Указаны ли где-нибудь карточки
local hasCards = false
local matchNumber = 1
while true do
local value = args["match" .. matchNumber]
if value == nil or trim(value) == "" then
break
end
local fields = parseMatchValue(value)
if not fields then
return "'''Ошибка:''' матч " ..
matchNumber ..
" имеет неправильный формат."
end
local tour = fields.tour
local round = fields.round
local home = fields.home
local away = fields.away
local score = fields.score
-- Проверяем команды
if not teams[home] then
return "'''Ошибка:''' в матче " ..
matchNumber ..
" неизвестная команда '''" ..
home .. "'''."
end
if not teams[away] then
return "'''Ошибка:''' в матче " ..
matchNumber ..
" неизвестная команда '''" ..
away .. "'''."
end
local parsed = parseScore(score)
local match = {
number = matchNumber,
tour = tour,
round = round,
home = home,
away = away,
played = parsed.played,
status = parsed.status,
homeGoals = parsed.homeGoals,
awayGoals = parsed.awayGoals,
score = parsed.score,
deductions = {}
}
-- -------------------------------------------------
-- КАРТОЧКИ В МАТЧЕ
-- -------------------------------------------------
-- matchN_yellow, matchN_yellowred, matchN_red
-- в формате «хозяева:гости»
for _, cardType in ipairs(CARD_TYPES) do
local paramName =
"match" .. matchNumber .. "_" .. cardType.key
local raw = trim(args[paramName])
if raw ~= "" then
local homeCards, awayCards = parsePair(raw)
if homeCards == nil then
return "'''Ошибка:''' параметр ''" ..
paramName ..
"'' имеет неправильный формат «" ..
raw ..
"». Укажите карточки хозяев и гостей " ..
"через двоеточие, например ''2:1''."
end
local homeCardsTable = teams[home].matchCards
local awayCardsTable = teams[away].matchCards
homeCardsTable[cardType.key] =
homeCardsTable[cardType.key] + homeCards
awayCardsTable[cardType.key] =
awayCardsTable[cardType.key] + awayCards
hasCards = true
end
end
-- -------------------------------------------------
-- СНЯТИЕ ОЧКОВ ЗА МАТЧ
-- -------------------------------------------------
-- matchN_deduction = код;очки;причина
for i = 1, MAX_DEDUCTIONS do
local paramName =
"match" .. matchNumber .. "_deduction"
if i > 1 then
paramName = paramName .. i
end
local raw = trim(args[paramName])
if raw ~= "" then
local deductionParts = split(raw, ";")
local code = deductionParts[1]
-- Допускается и «1,5», и «1.5»
local amount = tonumber(
(string.gsub(deductionParts[2] or "", ",", "."))
)
if code ~= home and code ~= away then
return "'''Ошибка:''' в параметре ''" ..
paramName ..
"'' должна быть указана команда матча " ..
matchNumber .. " ('''" .. home ..
"''' или '''" .. away ..
"'''), а указано «" .. code .. "»."
end
if amount == nil or
amount == 0 or
amount ~= amount or
math.abs(amount) == math.huge then
return "'''Ошибка:''' в параметре ''" ..
paramName ..
"'' не указано, сколько очков снято."
end
-- Причина — всё, что после второй «;»
local reason =
table.concat(deductionParts, "; ", 3)
reason = string.gsub(reason, "[%s%.]+$", "")
local deduction = {
team = code,
points = math.abs(amount),
reason = reason,
match = match
}
table.insert(match.deductions, deduction)
table.insert(deductions, deduction)
end
end
table.insert(matches, match)
-- -------------------------------------------------
-- СТАТИСТИКА
-- -------------------------------------------------
if parsed.played then
addResult(teams, match)
end
applyDeductions(teams, match)
matchNumber = matchNumber + 1
end
-- =====================================================
-- МАТЧИ ДЛЯ ЛИЧНЫХ ВСТРЕЧ
-- =====================================================
-- h2h_matchN — матчи, сыгранные до этого турнира (например,
-- в первом этапе), в тех же форматах, что и matchN.
-- Они учитываются только в критерии «личные» и не влияют
-- на показатели команд, шахматку и таблицу по турам.
-- Матчи с командами, которых нет в teams, пропускаются,
-- поэтому можно вставить все матчи первого этапа.
local h2hMatches = {}
local h2hNumber = 1
while true do
local paramName = "h2h_match" .. h2hNumber
local value = trim(args[paramName])
if value == "" then
break
end
local fields = parseMatchValue(value)
if not fields then
return "'''Ошибка:''' параметр ''" ..
paramName ..
"'' имеет неправильный формат."
end
local parsed = parseScore(fields.score)
if parsed.played and
teams[fields.home] and
teams[fields.away] then
table.insert(h2hMatches, {
home = fields.home,
away = fields.away,
homeGoals = parsed.homeGoals,
awayGoals = parsed.awayGoals
})
end
h2hNumber = h2hNumber + 1
end
-- =====================================================
-- КАРТОЧКИ КОМАНД
-- =====================================================
-- Параметры yellow_КОД, yellowred_КОД, red_КОД задают
-- итог команды. Если указан хотя бы один из них, карточки
-- команды берутся только из этих параметров (неуказанные
-- считаются нулём), а карточки из матчей и стартовых
-- показателей не учитываются.
for _, code in ipairs(teamOrder) do
local team = teams[code]
local override = {}
local hasOverride = false
for _, cardType in ipairs(CARD_TYPES) do
local paramName = cardType.key .. "_" .. code
local raw = trim(args[paramName])
if raw ~= "" then
if not raw:match("^%d+$") then
return "'''Ошибка:''' параметр ''" ..
paramName ..
"'' должен быть целым неотрицательным " ..
"числом, а указано «" .. raw .. "»."
end
override[cardType.key] = tonumber(raw)
hasOverride = true
end
end
for _, cardType in ipairs(CARD_TYPES) do
if hasOverride then
team[cardType.key] =
override[cardType.key] or 0
else
team[cardType.key] =
team.matchCards[cardType.key]
end
end
if hasOverride or
(team.start and team.start.hasCards) then
hasCards = true
end
end
-- =====================================================
-- РАЗНИЦА МЯЧЕЙ
-- =====================================================
for _, code in ipairs(teamOrder) do
local team = teams[code]
team.goalDifference =
team.goalsFor - team.goalsAgainst
end
-- =====================================================
-- ЛИЧНЫЕ ВСТРЕЧИ
-- =====================================================
-- Добавляет сыгранный матч к статистике личных встреч
-- команды code, если обе команды матча входят в группу.
local function addHeadToHeadMatch(result, code, group, match)
if not (group[match.home] and group[match.away]) then
return
end
local scored
local conceded
if match.home == code then
scored = match.homeGoals
conceded = match.awayGoals
elseif match.away == code then
scored = match.awayGoals
conceded = match.homeGoals
else
return
end
result.goalsFor =
result.goalsFor + scored
result.goalsAgainst =
result.goalsAgainst + conceded
if scored > conceded then
result.points =
result.points + pointsWin
result.wins =
result.wins + 1
elseif scored == conceded then
result.points =
result.points + pointsDraw
else
result.points =
result.points + pointsLoss
end
end
-- Возвращает статистику команды только в матчах
-- против команд из указанной группы.
-- Матчи h2h_matchN учитываются всегда, а матчи турнира —
-- если задан lastTour, только с номером тура не больше lastTour.
-- Снятие очков на личные встречи не влияет.
local function headToHead(code, group, lastTour)
local result = {
points = 0,
goalsFor = 0,
goalsAgainst = 0,
goalDifference = 0,
wins = 0
}
-- Матчи, сыгранные до этого турнира
for _, match in ipairs(h2hMatches) do
addHeadToHeadMatch(result, code, group, match)
end
for _, match in ipairs(matches) do
local inRange = true
if lastTour then
local tourNumber = tonumber(match.tour)
inRange =
tourNumber ~= nil and tourNumber <= lastTour
end
if match.played and inRange then
addHeadToHeadMatch(result, code, group, match)
end
end
result.goalDifference =
result.goalsFor - result.goalsAgainst
return result
end
-- =====================================================
-- КРИТЕРИИ ОПРЕДЕЛЕНИЯ МЕСТ
-- =====================================================
-- По умолчанию:
-- 1. очки
-- 2. разница мячей
-- 3. забитые мячи
-- 4. победы
-- 5. название
local criteria = {}
for i = 1, 10 do
local criterion = trim(args["tie" .. i])
if criterion ~= "" then
table.insert(criteria, criterion)
end
end
-- Если критерии не указаны
if #criteria == 0 then
criteria = {
"очки",
"разница",
"забитые",
"победы",
"название"
}
end
-- -----------------------------------------------------
-- Сравнение команд
-- -----------------------------------------------------
-- Возвращает функцию сравнения для table.sort.
-- teamsMap — команды, среди которых ищутся равные по очкам
-- (для критерия «личные»); lastTour — последний
-- учитываемый тур (nil — все матчи).
local function makeComparator(teamsMap, lastTour)
return function(a, b)
for _, criterion in ipairs(criteria) do
-- -----------------------------------------
-- ОЧКИ
-- -----------------------------------------
if criterion == "очки" then
if a.points ~= b.points then
return a.points > b.points
end
-- -----------------------------------------
-- РАЗНИЦА МЯЧЕЙ
-- -----------------------------------------
elseif criterion == "разница" then
if a.goalDifference ~= b.goalDifference then
return a.goalDifference > b.goalDifference
end
-- -----------------------------------------
-- ЗАБИТЫЕ МЯЧИ
-- -----------------------------------------
elseif criterion == "забитые" then
if a.goalsFor ~= b.goalsFor then
return a.goalsFor > b.goalsFor
end
-- -----------------------------------------
-- ПОБЕДЫ
-- -----------------------------------------
elseif criterion == "победы" then
if a.wins ~= b.wins then
return a.wins > b.wins
end
-- -----------------------------------------
-- ИГРЫ
-- -----------------------------------------
elseif criterion == "игры" then
if a.played ~= b.played then
return a.played > b.played
end
-- -----------------------------------------
-- ЛИЧНЫЕ
-- -----------------------------------------
elseif criterion == "личные" then
-- Все команды с таким же
-- количеством очков, как у a и b.
local group = {}
for _, code in ipairs(teamOrder) do
if teamsMap[code].points == a.points then
group[code] = true
end
end
local ha = headToHead(a.code, group, lastTour)
local hb = headToHead(b.code, group, lastTour)
if ha.points ~= hb.points then
return ha.points > hb.points
end
if ha.goalDifference ~= hb.goalDifference then
return ha.goalDifference > hb.goalDifference
end
if ha.goalsFor ~= hb.goalsFor then
return ha.goalsFor > hb.goalsFor
end
if ha.wins ~= hb.wins then
return ha.wins > hb.wins
end
-- -----------------------------------------
-- НАЗВАНИЕ
-- -----------------------------------------
elseif criterion == "название" then
local aname =
mw.text.unstrip(a.name)
local bname =
mw.text.unstrip(b.name)
if aname ~= bname then
return aname < bname
end
end
end
-- Последний резервный критерий —
-- код команды.
return a.code < b.code
end
end
-- =====================================================
-- НУМЕРАЦИЯ МЕСТ
-- =====================================================
-- first_place — номер первого места таблицы. Например,
-- в таблице группы за 7–14 места первая строка — 7-е место.
-- Места в highlightN и progress_highlightN указываются
-- в этой же нумерации.
local placeOffset = 0
local firstPlace = trim(args.first_place)
if firstPlace ~= "" then
if not firstPlace:match("^%d+$") or
tonumber(firstPlace) < 1 then
return "'''Ошибка:''' параметр ''first_place'' должен " ..
"быть целым положительным числом, а указано «" ..
firstPlace .. "»."
end
placeOffset = tonumber(firstPlace) - 1
end
-- =====================================================
-- Выделение победителей и проигравших
-- =====================================================
-- highlightN = место;количество;класс;текст
local highlights = {}
-- Есть ли текст хотя бы у одного выделения
local hasHighlightText = false
for i = 1, 15 do
local hl = trim(args["highlight" .. i])
if hl == "" then
break
end
local parts = split(hl, ";")
local place = tonumber(parts[1])
if place then
-- Номер строки таблицы с учётом first_place
local row = math.floor(place) - placeOffset
local count =
math.max(1, math.floor(tonumber(parts[2]) or 1))
-- Зона, которая начинается выше таблицы,
-- обрезается по первой строке
if row < 1 then
count = count + row - 1
row = 1
end
-- Зоны целиком за пределами таблицы пропускаются
if count >= 1 and row <= #teamOrder then
local highlight = {
place = row,
count = count,
color = parts[3] or "",
-- Текст может сам содержать «;»
text = table.concat(parts, "; ", 4)
}
if highlight.text ~= "" then
hasHighlightText = true
end
table.insert(highlights, highlight)
end
end
end
table.sort(highlights, function(a, b)
return a.place < b.place
end)
-- -----------------------------------------------------
-- Режим отображения шахматки
-- -----------------------------------------------------
-- По умолчанию первая строчка - домашние игры, вторая строчка - гостевые
-- Если resultsMode = round, то первая строчка - 1 круг, вторая строчка - 2 круг
local resultsMode = 0
if args["resultsMode"] ~= nil and args["resultsMode"] ~= "" then
if args["resultsMode"] == "round" then
resultsMode = 1
end
end
-- rounds = 1 — однокруговой турнир: у каждой команды в шахматке
-- одна строка, в ячейке — единственный матч пары (resultsMode
-- при этом не действует). rounds = 2 или пусто — двухкруговой.
local roundsParameter = trim(args.rounds)
if roundsParameter ~= "" and
roundsParameter ~= "1" and
roundsParameter ~= "2" then
return "'''Ошибка:''' параметр ''rounds'' может быть равен " ..
"1 (однокруговой турнир) или 2 (двухкруговой), " ..
"а указано «" .. roundsParameter .. "»."
end
local singleRound = roundsParameter == "1"
-- =====================================================
-- ПРОГРЕСС ПО ТУРАМ
-- =====================================================
local function buildProgress()
local progress = {}
-- Собираем список туров
local tours = {}
for _, match in ipairs(matches) do
local tourNumber = tonumber(match.tour)
if tourNumber then
tours[tourNumber] = true
end
end
local tourNumbers = {}
for tourNumber in pairs(tours) do
table.insert(tourNumbers, tourNumber)
end
table.sort(tourNumbers)
-- Таблица, которая последовательно
-- пересчитывается после каждого тура
local currentTeams = {}
for _, code in ipairs(teamOrder) do
currentTeams[code] = newTeam(
code,
teams[code].name
)
-- Стартовые показатели действуют с первого тура
addStart(currentTeams[code], teams[code].start)
end
for _, tourNumber in ipairs(tourNumbers) do
-- Добавляем матчи данного тура
for _, match in ipairs(matches) do
if tonumber(match.tour) == tourNumber then
if match.played then
addResult(currentTeams, match)
end
-- Снятие очков действует с этого тура
applyDeductions(currentTeams, match)
end
end
-- Разница мячей
for _, code in ipairs(teamOrder) do
local team = currentTeams[code]
team.goalDifference =
team.goalsFor - team.goalsAgainst
end
-- Таблица после этого тура
local tourStandings = {}
for _, code in ipairs(teamOrder) do
table.insert(
tourStandings,
currentTeams[code]
)
end
table.sort(
tourStandings,
makeComparator(currentTeams, tourNumber)
)
progress[tourNumber] = {}
for position, team in ipairs(tourStandings) do
progress[tourNumber][team.code] =
position
end
end
return progress, tourNumbers
end
-- =====================================================
-- СОРТИРОВКА
-- =====================================================
local standings = {}
for _, code in ipairs(teamOrder) do
table.insert(standings, teams[code])
end
table.sort(standings, makeComparator(teams, nil))
local teamCount = #standings
-- =====================================================
-- Подсветка
-- =====================================================
-- Цвет строк
for _, hl in ipairs(highlights) do
local first = math.max(hl.place, 1)
local last = math.min(hl.place + hl.count - 1, teamCount)
for position = first, last do
standings[position].highlightColor = hl.color
end
end
-- Ячейки столбца «Повышение или выбывание».
-- Каждая зона — одна объединённая ячейка с текстом,
-- места между зонами — объединённые пустые ячейки.
if hasHighlightText then
local position = 1
local index = 1
while position <= teamCount do
-- Пропускаем зоны, которые начинаются выше
-- текущей строки (пересекаются с предыдущей)
while highlights[index] and
highlights[index].place < position do
index = index + 1
end
local hl = highlights[index]
local span
local text
if hl and hl.place == position then
span = math.min(hl.count, teamCount - position + 1)
text = hl.text
index = index + 1
else
local nextPlace = teamCount + 1
if hl then
nextPlace = math.min(hl.place, teamCount + 1)
end
span = nextPlace - position
text = ""
end
standings[position].zoneCell = {
text = text,
span = span
}
position = position + span
end
end
-- =====================================================
-- ПРИМЕЧАНИЯ О СНЯТИИ ОЧКОВ
-- =====================================================
-- Текст примечания, например:
-- «С команды «Спартак» снято 3 очка в 5-м туре
-- (матч «Спартак» — «Локомотив») за неявку на матч.»
local function deductionText(deduction)
local match = deduction.match
local text =
"С команды «" .. teams[deduction.team].name ..
"» снято " .. formatNumber(deduction.points) .. " " ..
plural(deduction.points, "очко", "очка", "очков")
local tourNumber = tonumber(match.tour)
if tourNumber then
text = text .. " в " ..
formatNumber(tourNumber) .. "-м туре"
end
text = text ..
" (матч «" .. teams[match.home].name ..
"» — «" .. teams[match.away].name .. "»)"
if deduction.reason ~= "" then
text = text .. " " .. deduction.reason
end
return text .. "."
end
-- Сноски нумеруются сверху вниз по итоговой таблице
local notes = {}
for _, team in ipairs(standings) do
for _, deduction in ipairs(deductions) do
if deduction.team == team.code then
local label = noteLabel(#notes + 1)
local note = {
label = label,
id = idPrefix .. "-note-" .. label,
refId = idPrefix .. "-ref-" .. label,
text = deductionText(deduction)
}
deduction.note = note
table.insert(notes, note)
table.insert(team.notes, note)
end
end
end
-- Сноска вида [a]. Сноска в итоговой таблице получает id,
-- чтобы из примечания можно было вернуться к ней (↑).
local function noteMarker(note, withAnchor)
local marker =
mw.html.create("sup")
:addClass("reference")
:addClass("football-note-ref")
:wikitext(
"[[#" .. note.id .. "|[" ..
note.label .. "]]]"
)
if withAnchor then
marker:attr("id", note.refId)
end
return marker
end
-- =====================================================
-- ТАБЛИЦА ЧЕМПИОНАТА
-- =====================================================
local root =
mw.html.create("div")
:addClass("football-tables")
local standingsTable =
mw.html.create("table")
:addClass("wikitable")
:addClass("football-standings")
:css("text-align", "center")
-- Заголовок
local standingsHeader =
mw.html.create("tr")
local headings = {
"Место",
"Команда",
"И",
"В",
"Н",
"П",
"МЗ",
"МП",
"РМ",
"О"
}
for _, title in ipairs(headings) do
standingsHeader:node(
cell("th", title)
)
end
-- Столбцы карточек выводятся, только если
-- карточки где-нибудь указаны
if hasCards then
for _, cardType in ipairs(CARD_TYPES) do
standingsHeader:node(
mw.html.create("th")
:addClass("football-cards")
:attr("title", cardType.title)
:node(cardIcon(cardType))
)
end
end
if hasHighlightText then
standingsHeader:node(
cell("th", "Повышение или выбывание")
)
end
standingsTable:node(standingsHeader)
-- Строки
for position, team in ipairs(standings) do
local row =
mw.html.create("tr")
if team.highlightColor and team.highlightColor ~= "" then
row:addClass("highlight"):addClass(team.highlightColor)
end
row:node(
cell("td", position + placeOffset)
)
-- Название и сноски о снятии очков
local teamName =
cell("td", team.name)
:css("text-align", "left")
for _, note in ipairs(team.notes) do
teamName:node(
noteMarker(note, true)
)
end
row:node(
teamName
)
row:node(
cell("td", team.played)
)
row:node(
cell("td", team.wins)
)
row:node(
cell("td", team.draws)
)
row:node(
cell("td", team.losses)
)
row:node(
cell("td", team.goalsFor)
)
row:node(
cell("td", team.goalsAgainst)
)
local gd
if team.goalDifference > 0 then
gd = "+" .. team.goalDifference
else
gd = tostring(team.goalDifference)
end
row:node(
cell("td", gd)
)
local pointsCell =
cell("td", formatNumber(team.points))
if team.deducted > 0 then
pointsCell:attr(
"title",
"Снято " .. formatNumber(team.deducted) .. " " ..
plural(team.deducted, "очко", "очка", "очков")
)
end
row:node(
pointsCell
)
if hasCards then
for _, cardType in ipairs(CARD_TYPES) do
row:node(
cell("td", team[cardType.key], "football-cards")
)
end
end
if hasHighlightText and team.zoneCell then
local zone =
cell("td", team.zoneCell.text)
if team.zoneCell.span > 1 then
zone:attr("rowspan", team.zoneCell.span)
end
row:node(
zone
)
end
standingsTable:node(row)
end
root:node(standingsTable)
-- =====================================================
-- ИНФОРМАЦИЯ О КРИТЕРИЯХ
-- =====================================================
local criteriaText = table.concat(criteria, " → ")
local info =
mw.html.create("div")
:addClass("football-info")
:wikitext(
"Критерии определения мест: " ..
criteriaText
)
root:node(info)
-- =====================================================
-- ПРИМЕЧАНИЯ ПОД ТАБЛИЦЕЙ
-- =====================================================
if #notes > 0 then
local notesBlock =
mw.html.create("div")
:addClass("football-info")
:addClass("football-notes")
:wikitext("Примечания:")
local list =
notesBlock:tag("ol")
:css("list-style-type", "lower-alpha")
for _, note in ipairs(notes) do
local item =
list:tag("li")
:attr("id", note.id)
item:tag("span")
:addClass("mw-cite-backlink")
:wikitext("[[#" .. note.refId .. "|↑]]")
item:wikitext(" ")
item:tag("span")
:addClass("reference-text")
:wikitext(note.text)
end
root:node(notesBlock)
end
-- =====================================================
-- РЕЗУЛЬТАТЫ
-- =====================================================
root:wikitext(heading("Результаты"))
local resultsTable =
mw.html.create("table")
:addClass("wikitable")
:addClass("football-results")
:css("text-align", "center")
-- Заголовок
local header =
mw.html.create("tr")
header:node(
cell("th", "", "football-corner")
)
for _, code in ipairs(teamOrder) do
header:node(
cell(
"th",
teams[code].name,
"football-team-header"
)
)
end
resultsTable:node(header)
-- Матрица результатов
-- Матч для ячейки шахматки.
-- eitherVenue = false — только матч на поле homeCode,
-- true — матч этой пары на любом поле (однокруговой турнир).
-- Если подходящих матчей несколько, берётся первый сыгранный,
-- если сыгранных нет — первый с пометкой («перенесён» и т. п.),
-- иначе просто первый.
local function findMatch(homeCode, awayCode, eitherVenue)
local found
local foundRank = 0
for _, match in ipairs(matches) do
local suits =
(match.home == homeCode and match.away == awayCode) or
(eitherVenue and
match.home == awayCode and match.away == homeCode)
if suits then
local rank = 1
if match.played then
rank = 3
elseif match.status ~= "" then
rank = 2
end
if rank > foundRank then
found = match
foundRank = rank
end
end
end
return found
end
-- Ячейка шахматки для команды строки rowCode.
-- Счёт записан как «хозяева:гости»,
-- а цвет показывает результат команды строки.
local function makeResultCell(match, rowCode)
local resultCell =
mw.html.create("td")
if not match then
return resultCell:wikitext("—")
end
if match.played then
local scored = match.homeGoals
local conceded = match.awayGoals
if match.away == rowCode then
scored = match.awayGoals
conceded = match.homeGoals
end
if scored > conceded then
resultCell
:addClass("ui")
:addClass("green")
elseif scored < conceded then
resultCell
:addClass("ui")
:addClass("red")
else
resultCell
:addClass("ui")
:addClass("yellow")
end
resultCell:wikitext(match.score)
elseif match.status ~= "" then
resultCell:wikitext(match.status)
else
resultCell:wikitext("—")
end
return resultCell
end
-- Строк у команды: в двухкруговом турнире две
-- (дома и в гостях или 1-й и 2-й круг), в однокруговом — одна
local rowsPerTeam = 2
if singleRound then
rowsPerTeam = 1
end
for _, homeCode in ipairs(teamOrder) do
local homeRow =
mw.html.create("tr")
local awayRow =
mw.html.create("tr")
local teamHeader =
cell(
"th",
teams[homeCode].name,
"football-team-header"
)
if rowsPerTeam > 1 then
teamHeader:attr("rowspan", rowsPerTeam)
end
teamHeader:css("text-align", "right")
homeRow:node(teamHeader)
for _, awayCode in ipairs(teamOrder) do
if homeCode == awayCode then
local diagonal = cell("td", "—", "football-diagonal")
if rowsPerTeam > 1 then
diagonal:attr("rowspan", rowsPerTeam)
end
homeRow:node(
diagonal
)
elseif singleRound then
-- Единственный матч пары, на чьём бы поле он ни был
homeRow:node(
makeResultCell(
findMatch(homeCode, awayCode, true),
homeCode
)
)
else
local homeMatch = findMatch(homeCode, awayCode, false)
local awayMatch = findMatch(awayCode, homeCode, false)
local homeResultCell = makeResultCell(homeMatch, homeCode)
local awayResultCell = makeResultCell(awayMatch, homeCode)
-- Нужно ли поменять местами гостевую и домашнюю игры:
-- в режиме по кругам первая строка — матч 1-го круга,
-- вторая — матч 2-го круга
local changeOrder =
resultsMode == 1 and (
(homeMatch ~= nil and homeMatch.round == "2") or
(awayMatch ~= nil and awayMatch.round == "1")
)
if changeOrder then
homeRow:node(awayResultCell)
awayRow:node(homeResultCell)
else
homeRow:node(homeResultCell)
awayRow:node(awayResultCell)
end
end
end
resultsTable:node(homeRow)
if rowsPerTeam > 1 then
resultsTable:node(awayRow)
end
end
root:node(resultsTable)
-- =====================================================
-- ПРОГРЕСС ПО ТУРАМ
-- =====================================================
local showProgress = trim(args.show_progress) == "yes"
local progress
local tourNumbers
if showProgress then
progress, tourNumbers =
buildProgress()
root:wikitext(heading("Положение команд по турам"))
-- Без номеров туров строить нечего
if #tourNumbers == 0 then
root:node(
mw.html.create("div")
:addClass("football-info")
:wikitext(
"''Нет матчей с номером тура. " ..
"Для этой таблицы матчи нужно указывать " ..
"в формате «тур;круг;хозяева;гости;счёт».''"
)
)
showProgress = false
end
end
if showProgress then
local progressTable =
mw.html.create("table")
:addClass("wikitable")
:addClass("football-progress")
:css("text-align", "center")
-- -------------------------------------------------
-- Цвета мест
-- -------------------------------------------------
local progressHighlights = {}
for i = 1, 15 do
local value =
trim(args["progress_highlight" .. i])
if value ~= "" then
local parts =
split(value, ";")
local place =
tonumber(parts[1])
local color =
parts[2] or ""
if place and color ~= "" then
progressHighlights[place] =
color
end
end
end
-- -------------------------------------------------
-- Сноски о снятии очков: тур → команда → сноски
-- -------------------------------------------------
local progressNotes = {}
for _, deduction in ipairs(deductions) do
local tourNumber = tonumber(deduction.match.tour)
if tourNumber and deduction.note then
progressNotes[tourNumber] =
progressNotes[tourNumber] or {}
local teamNotes =
progressNotes[tourNumber][deduction.team] or {}
table.insert(teamNotes, deduction.note)
progressNotes[tourNumber][deduction.team] =
teamNotes
end
end
-- -------------------------------------------------
-- Заголовок
-- -------------------------------------------------
local progressHeader =
mw.html.create("tr")
progressHeader:node(
cell("th", "Команда")
:attr("rowspan", 2)
)
progressHeader:node(
cell("th", "Тур")
:attr("colspan", #tourNumbers)
)
progressTable:node(progressHeader)
local tourHeader =
mw.html.create("tr")
for _, tourNumber in ipairs(tourNumbers) do
tourHeader:node(
cell(
"th",
tourNumber
)
)
end
progressTable:node(tourHeader)
-- -------------------------------------------------
-- Строки команд
-- -------------------------------------------------
for _, code in ipairs(teamOrder) do
local row =
mw.html.create("tr")
row:node(
cell(
"th",
teams[code].name
)
)
for _, tourNumber in ipairs(tourNumbers) do
-- Место с учётом first_place
local position =
progress[tourNumber][code] + placeOffset
local resultCell =
cell("td", position)
local color =
progressHighlights[position]
if color and color ~= "" then
resultCell
:addClass("highlight")
:addClass(color)
end
-- В туре, где сняты очки, — та же сноска,
-- что и в итоговой таблице
local cellNotes =
progressNotes[tourNumber] and
progressNotes[tourNumber][code]
if cellNotes then
resultCell:addClass("football-deduction")
for _, note in ipairs(cellNotes) do
resultCell:node(
noteMarker(note, false)
)
end
end
row:node(resultCell)
end
progressTable:node(row)
end
root:node(progressTable)
end
return tostring(root)
end
return p