Перейти к содержанию

Модуль:Football: различия между версиями

Добавлен вывод ЖК, и реализована возможность списывать очки
Нет описания правки
 
(не показаны 3 промежуточные версии этого же участника)
Строка 28: Строка 28:
-- matchN_deduction, matchN_deduction2 … matchN_deduction5
-- matchN_deduction, matchN_deduction2 … matchN_deduction5
local MAX_DEDUCTIONS = 5
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




Строка 173: Строка 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


Строка 201: Строка 280:
notes = {},
notes = {},


-- Карточки, собранные по матчам
-- Карточки из стартовых показателей и матчей
matchCards = {
matchCards = {
yellow = 0,
yellow = 0,
Строка 213: Строка 292:
red = 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


Строка 335: Строка 446:




-- Префикс якорей для сносок. Он должен быть уникальным
-- -----------------------------------------------------
-- на странице, поэтому по умолчанию зависит от списка команд.
-- СТАРТОВЫЕ ПОКАЗАТЕЛИ
local idPrefix = trim(args.id)
-- -----------------------------------------------------


if idPrefix == "" then
-- start_КОД = И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК — показатели,
-- с которыми команда начинает турнир. Нужны, например,
-- когда после первого этапа таблица делится на группы:
-- таблица каждой группы начинается с итогов первого этапа.
-- Пустая часть считается нулём; пустые И и О вычисляются
-- по победам, ничьим и поражениям. Параметры для кодов,
-- которых нет в teams, не читаются.


idPrefix = "football"
-- Возвращает показатели (nil, если параметр не указан)
-- или nil и текст ошибки
local function parseStart(code)


if mw.hash and mw.hash.hashValue then
local paramName = "start_" .. code
local raw = trim(args[paramName])


local ok, hash = pcall(
if raw == "" then
mw.hash.hashValue,
return nil
"crc32b",
end
teamsParameter
)


if ok and hash then
local function fail(text)
idPrefix = idPrefix .. "-" .. hash
return nil,
end
"'''Ошибка:''' в параметре ''" ..
paramName .. "'' " .. text
end


end
local parts = split(raw, ";")


else
if #parts > #START_PARTS then
-- Символы, которые ломают якорь или ссылку [[#…]]
idPrefix = string.gsub(idPrefix, "[%s%[%]{}|#<>\"'&]+", "_")
end


return fail(
"больше " .. #START_PARTS .. " значений. " ..
"Формат: ''" .. START_FORMAT .. "''."
)


-- -----------------------------------------------------
end
-- УЧЁТ РЕЗУЛЬТАТА И СНЯТИЯ ОЧКОВ
-- -----------------------------------------------------


-- Добавляет результат сыгранного матча в статистику.
local start = {
-- teamsMap — итоговые команды или команды «по турам».
-- Указаны ли карточки
local function addResult(teamsMap, match)
hasCards = false
}


local homeTeam = teamsMap[match.home]
for index, part in ipairs(START_PARTS) do
local awayTeam = teamsMap[match.away]


homeTeam.played = homeTeam.played + 1
local value = parts[index] or ""
awayTeam.played = awayTeam.played + 1


homeTeam.goalsFor =
if part.key == "points" then
homeTeam.goalsFor + match.homeGoals


homeTeam.goalsAgainst =
-- Очки могут быть дробными и отрицательными:
homeTeam.goalsAgainst + match.awayGoals
-- «12,5», «-3» (пустые вычисляются ниже)
if value ~= "" then


awayTeam.goalsFor =
local normalized =
awayTeam.goalsFor + match.awayGoals
string.gsub(value, ",", ".")


awayTeam.goalsAgainst =
normalized =
awayTeam.goalsAgainst + match.homeGoals
string.gsub(normalized, "−", "-")


local amount = tonumber(normalized)


if match.homeGoals > match.awayGoals then
if amount == nil or
  amount ~= amount or
  math.abs(amount) == math.huge then


homeTeam.wins = homeTeam.wins + 1
return fail(
awayTeam.losses = awayTeam.losses + 1
"очки (О) должны быть числом, " ..
"а указано «" .. value .. "»."
)


homeTeam.points =
end
homeTeam.points + pointsWin


awayTeam.points =
start.points = amount
awayTeam.points + pointsLoss


end


elseif match.homeGoals < match.awayGoals then
elseif value == "" then


awayTeam.wins = awayTeam.wins + 1
-- Пустое число игр вычисляется ниже
homeTeam.losses = homeTeam.losses + 1
if part.key ~= "played" then
start[part.key] = 0
end


awayTeam.points =
elseif value:match("^%d+$") then
awayTeam.points + pointsWin


homeTeam.points =
start[part.key] = tonumber(value)
homeTeam.points + pointsLoss


if part.card then
start.hasCards = true
end


else
else


homeTeam.draws = homeTeam.draws + 1
return fail(
awayTeam.draws = awayTeam.draws + 1
"значение " .. part.label ..
" должно быть целым неотрицательным числом, " ..
"а указано «" .. value .. "»."
)


homeTeam.points =
end
homeTeam.points + pointsDraw
 
awayTeam.points =
awayTeam.points + pointsDraw


end
end


end


local results =
start.wins + start.draws + start.losses


-- Снимает очки, привязанные к матчу
if start.played == nil then
-- (независимо от того, сыгран ли он)
local function applyDeductions(teamsMap, match)


for _, deduction in ipairs(match.deductions) do
start.played = results


local team = teamsMap[deduction.team]
elseif start.played ~= results then


team.points =
return fail(
team.points - deduction.points
"число игр (" .. start.played .. ") не равно " ..
 
"сумме побед, ничьих и поражений (" ..
team.deducted =
results .. ")."
team.deducted + deduction.points
)


end
end


end


if start.points == nil then


-- -----------------------------------------------------
start.points =
-- МАТЧИ
start.wins * pointsWin +
-- -----------------------------------------------------
start.draws * pointsDraw +
start.losses * pointsLoss


local matches = {}
end


-- Все снятия очков в порядке матчей
return start
local deductions = {}
end
 
-- Указаны ли где-нибудь карточки
local hasCards = false


local matchNumber = 1


while true do
for _, code in ipairs(teamOrder) do


local value = args["match" .. matchNumber]
local start, errorText = parseStart(code)


if value == nil or trim(value) == "" then
if errorText then
break
return errorText
end
end


local parts = split(value, ";")
if start then


local tour
teams[code].start = start
local round
local home
local away
local score


-- Формат с номером тура:
addStart(teams[code], start)
-- тур;круг;хозяева;гости;счёт
if #parts >= 5 then
tour = trim(parts[1])
round = trim(parts[2])
home = trim(parts[3])
away = trim(parts[4])
score = trim(parts[5])


-- Формат с номером круга:
end
-- круг;хозяева;гости;счёт
elseif #parts >= 4 then
tour = ""
round = trim(parts[1])
home = trim(parts[2])
away = trim(parts[3])
score = trim(parts[4])


-- Формат без номера круга:
end
-- хозяева;гости;счёт
elseif #parts >= 3 then
tour = ""
round = ""
home = trim(parts[1])
away = trim(parts[2])
score = trim(parts[3])


else


return "'''Ошибка:''' матч " ..
-- Префикс якорей для сносок. Он должен быть уникальным
matchNumber ..
-- на странице, поэтому по умолчанию зависит от списка команд.
" имеет неправильный формат."
local idPrefix = trim(args.id)


end
if idPrefix == "" then


idPrefix = "football"


-- Проверяем команды
if mw.hash and mw.hash.hashValue then


if not teams[home] then
local ok, hash = pcall(
mw.hash.hashValue,
"crc32b",
teamsParameter
)


return "'''Ошибка:''' в матче " ..
if ok and hash then
matchNumber ..
idPrefix = idPrefix .. "-" .. hash
" неизвестная команда '''" ..
end
home .. "'''."


end
end


if not teams[away] then
else
-- Символы, которые ломают якорь или ссылку [[#…]]
idPrefix = string.gsub(idPrefix, "[%s%[%]{}|#<>\"'&]+", "_")
end


return "'''Ошибка:''' в матче " ..
matchNumber ..
" неизвестная команда '''" ..
away .. "'''."


end
-- -----------------------------------------------------
-- УЧЁТ РЕЗУЛЬТАТА И СНЯТИЯ ОЧКОВ
-- -----------------------------------------------------


-- Добавляет результат сыгранного матча в статистику.
-- teamsMap — итоговые команды или команды «по турам».
local function addResult(teamsMap, match)


local parsed = parseScore(score)
local homeTeam = teamsMap[match.home]
local awayTeam = teamsMap[match.away]


local match = {
homeTeam.played = homeTeam.played + 1
number = matchNumber,
awayTeam.played = awayTeam.played + 1
tour = tour,
round = round,


home = home,
homeTeam.goalsFor =
away = away,
homeTeam.goalsFor + match.homeGoals


played = parsed.played,
homeTeam.goalsAgainst =
status = parsed.status,
homeTeam.goalsAgainst + match.awayGoals


homeGoals = parsed.homeGoals,
awayTeam.goalsFor =
awayGoals = parsed.awayGoals,
awayTeam.goalsFor + match.awayGoals
score = parsed.score,


deductions = {}
awayTeam.goalsAgainst =
}
awayTeam.goalsAgainst + match.homeGoals




-- -------------------------------------------------
if match.homeGoals > match.awayGoals then
-- КАРТОЧКИ В МАТЧЕ
-- -------------------------------------------------


-- matchN_yellow, matchN_yellowred, matchN_red
homeTeam.wins = homeTeam.wins + 1
-- в формате «хозяева:гости»
awayTeam.losses = awayTeam.losses + 1


for _, cardType in ipairs(CARD_TYPES) do
homeTeam.points =
homeTeam.points + pointsWin


local paramName =
awayTeam.points =
"match" .. matchNumber .. "_" .. cardType.key
awayTeam.points + pointsLoss


local raw = trim(args[paramName])


if raw ~= "" then
elseif match.homeGoals < match.awayGoals then


local homeCards, awayCards = parsePair(raw)
awayTeam.wins = awayTeam.wins + 1
homeTeam.losses = homeTeam.losses + 1


if homeCards == nil then
awayTeam.points =
awayTeam.points + pointsWin


return "'''Ошибка:''' параметр ''" ..
homeTeam.points =
paramName ..
homeTeam.points + pointsLoss
"'' имеет неправильный формат «" ..
raw ..
"». Укажите карточки хозяев и гостей " ..
"через двоеточие, например ''2:1''."


end


local homeCardsTable = teams[home].matchCards
else
local awayCardsTable = teams[away].matchCards


homeCardsTable[cardType.key] =
homeTeam.draws = homeTeam.draws + 1
homeCardsTable[cardType.key] + homeCards
awayTeam.draws = awayTeam.draws + 1


awayCardsTable[cardType.key] =
homeTeam.points =
awayCardsTable[cardType.key] + awayCards
homeTeam.points + pointsDraw


hasCards = true
awayTeam.points =
 
awayTeam.points + pointsDraw
end


end
end


end


-- -------------------------------------------------
-- СНЯТИЕ ОЧКОВ ЗА МАТЧ
-- -------------------------------------------------


-- matchN_deduction = код;очки;причина
-- Снимает очки, привязанные к матчу
-- (независимо от того, сыгран ли он)
local function applyDeductions(teamsMap, match)


for i = 1, MAX_DEDUCTIONS do
for _, deduction in ipairs(match.deductions) do


local paramName =
local team = teamsMap[deduction.team]
"match" .. matchNumber .. "_deduction"


if i > 1 then
team.points =
paramName = paramName .. i
team.points - deduction.points
end


local raw = trim(args[paramName])
team.deducted =
team.deducted + deduction.points


if raw ~= "" then
end


local deductionParts = split(raw, ";")
end


local code = deductionParts[1]


-- Допускается и «1,5», и «1.5»
-- -----------------------------------------------------
local amount = tonumber(
-- МАТЧИ
(string.gsub(deductionParts[2] or "", ",", "."))
-- -----------------------------------------------------
)


if code ~= home and code ~= away then
local matches = {}


return "'''Ошибка:''' в параметре ''" ..
-- Все снятия очков в порядке матчей
paramName ..
local deductions = {}
"'' должна быть указана команда матча " ..
matchNumber .. " ('''" .. home ..
"''' или '''" .. away ..
"'''), а указано «" .. code .. "»."


end
-- Указаны ли где-нибудь карточки
local hasCards = false


if amount == nil or
local matchNumber = 1
  amount == 0 or
  amount ~= amount or
  math.abs(amount) == math.huge then


return "'''Ошибка:''' в параметре ''" ..
while true do
paramName ..
"'' не указано, сколько очков снято."


end
local value = args["match" .. matchNumber]


-- Причина — всё, что после второй «;»
if value == nil or trim(value) == "" then
local reason =
break
table.concat(deductionParts, "; ", 3)
end


reason = string.gsub(reason, "[%s%.]+$", "")
local fields = parseMatchValue(value)


local deduction = {
if not fields then
team = code,
points = math.abs(amount),
reason = reason,
match = match
}


table.insert(match.deductions, deduction)
return "'''Ошибка:''' матч " ..
table.insert(deductions, deduction)
matchNumber ..
 
" имеет неправильный формат."
end


end
end


local tour = fields.tour
local round = fields.round
local home = fields.home
local away = fields.away
local score = fields.score


table.insert(matches, match)


-- Проверяем команды


-- -------------------------------------------------
if not teams[home] then
-- СТАТИСТИКА
 
-- -------------------------------------------------
return "'''Ошибка:''' в матче " ..
matchNumber ..
" неизвестная команда '''" ..
home .. "'''."


if parsed.played then
addResult(teams, match)
end
end


applyDeductions(teams, match)
if not teams[away] then


return "'''Ошибка:''' в матче " ..
matchNumber ..
" неизвестная команда '''" ..
away .. "'''."


matchNumber = matchNumber + 1
end
end




-- =====================================================
local parsed = parseScore(score)
-- КАРТОЧКИ КОМАНД
-- =====================================================


-- Параметры yellow_КОД, yellowred_КОД, red_КОД задают
local match = {
-- итог команды. Если указан хотя бы один из них, карточки
number = matchNumber,
-- команды берутся только из этих параметров (неуказанные
tour = tour,
-- считаются нулём), а карточки из матчей не учитываются.
round = round,


for _, code in ipairs(teamOrder) do
home = home,
away = away,


local team = teams[code]
played = parsed.played,
status = parsed.status,
 
homeGoals = parsed.homeGoals,
awayGoals = parsed.awayGoals,
score = parsed.score,
 
deductions = {}
}
 
 
-- -------------------------------------------------
-- КАРТОЧКИ В МАТЧЕ
-- -------------------------------------------------


local override = {}
-- matchN_yellow, matchN_yellowred, matchN_red
local hasOverride = false
-- в формате «хозяева:гости»


for _, cardType in ipairs(CARD_TYPES) do
for _, cardType in ipairs(CARD_TYPES) do


local paramName = cardType.key .. "_" .. code
local paramName =
"match" .. matchNumber .. "_" .. cardType.key
 
local raw = trim(args[paramName])
local raw = trim(args[paramName])


if raw ~= "" then
if raw ~= "" then


if not raw:match("^%d+$") then
local homeCards, awayCards = parsePair(raw)
 
if homeCards == nil then


return "'''Ошибка:''' параметр ''" ..
return "'''Ошибка:''' параметр ''" ..
paramName ..
paramName ..
"'' должен быть целым неотрицательным " ..
"'' имеет неправильный формат «" ..
"числом, а указано «" .. raw .. "»."
raw ..
"». Укажите карточки хозяев и гостей " ..
"через двоеточие, например ''2:1''."


end
end


override[cardType.key] = tonumber(raw)
local homeCardsTable = teams[home].matchCards
hasOverride = true
local awayCardsTable = teams[away].matchCards


end
homeCardsTable[cardType.key] =
homeCardsTable[cardType.key] + homeCards


end
awayCardsTable[cardType.key] =
awayCardsTable[cardType.key] + awayCards


for _, cardType in ipairs(CARD_TYPES) do
hasCards = true


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 then
hasCards = true
end


end
-- -------------------------------------------------
-- СНЯТИЕ ОЧКОВ ЗА МАТЧ
-- -------------------------------------------------


-- matchN_deduction = код;очки;причина


-- =====================================================
for i = 1, MAX_DEDUCTIONS do
-- РАЗНИЦА МЯЧЕЙ
-- =====================================================


for _, code in ipairs(teamOrder) do
local paramName =
"match" .. matchNumber .. "_deduction"


local team = teams[code]
if i > 1 then
paramName = paramName .. i
end


team.goalDifference =
local raw = trim(args[paramName])
team.goalsFor - team.goalsAgainst


end
if raw ~= "" then


local deductionParts = split(raw, ";")


-- =====================================================
local code = deductionParts[1]
-- ЛИЧНЫЕ ВСТРЕЧИ
-- =====================================================


-- Возвращает статистику команды только в матчах
-- Допускается и «1,5», и «1.5»
-- против команд из указанной группы.
local amount = tonumber(
-- Если задан lastTour, учитываются только матчи
(string.gsub(deductionParts[2] or "", ",", "."))
-- с номером тура не больше lastTour.
)
-- Снятие очков на личные встречи не влияет.


local function headToHead(code, group, lastTour)
if code ~= home and code ~= away then


local result = {
return "'''Ошибка:''' в параметре ''" ..
points = 0,
paramName ..
goalsFor = 0,
"'' должна быть указана команда матча " ..
goalsAgainst = 0,
matchNumber .. " ('''" .. home ..
goalDifference = 0,
"''' или '''" .. away ..
wins = 0
"'''), а указано «" .. code .. "»."
}


for _, match in ipairs(matches) do
end


local inRange = true
if amount == nil or
  amount == 0 or
  amount ~= amount or
  math.abs(amount) == math.huge then


if lastTour then
return "'''Ошибка:''' в параметре ''" ..
local tourNumber = tonumber(match.tour)
paramName ..
"'' не указано, сколько очков снято."


inRange =
end
tourNumber ~= nil and tourNumber <= lastTour
end


if match.played and
-- Причина — всё, что после второй «;»
  inRange and
local reason =
  group[match.home] and
table.concat(deductionParts, "; ", 3)
  group[match.away] then


local scored
reason = string.gsub(reason, "[%s%.]+$", "")
local conceded


if match.home == code then
local deduction = {
scored = match.homeGoals
team = code,
conceded = match.awayGoals
points = math.abs(amount),
elseif match.away == code then
reason = reason,
scored = match.awayGoals
match = match
conceded = match.homeGoals
}
end


if scored then
table.insert(match.deductions, deduction)
table.insert(deductions, deduction)


result.goalsFor =
end
result.goalsFor + scored


result.goalsAgainst =
end
result.goalsAgainst + conceded


if scored > conceded then


result.points =
table.insert(matches, match)
result.points + pointsWin


result.wins =
result.wins + 1


elseif scored == conceded then
-- -------------------------------------------------
-- СТАТИСТИКА
-- -------------------------------------------------


result.points =
if parsed.played then
result.points + pointsDraw
addResult(teams, match)
end


else
applyDeductions(teams, match)


result.points =
result.points + pointsLoss


end
matchNumber = matchNumber + 1
end


end


end
-- =====================================================
-- МАТЧИ ДЛЯ ЛИЧНЫХ ВСТРЕЧ
-- =====================================================


end
-- h2h_matchN — матчи, сыгранные до этого турнира (например,
-- в первом этапе), в тех же форматах, что и matchN.
-- Они учитываются только в критерии «личные» и не влияют
-- на показатели команд, шахматку и таблицу по турам.
-- Матчи с командами, которых нет в teams, пропускаются,
-- поэтому можно вставить все матчи первого этапа.


result.goalDifference =
local h2hMatches = {}
result.goalsFor - result.goalsAgainst


return result
local h2hNumber = 1
end


while true do


-- =====================================================
local paramName = "h2h_match" .. h2hNumber
-- КРИТЕРИИ ОПРЕДЕЛЕНИЯ МЕСТ
local value = trim(args[paramName])
-- =====================================================


-- По умолчанию:
if value == "" then
-- 1. очки
break
-- 2. разница мячей
end
-- 3. забитые мячи
-- 4. победы
-- 5. название


local criteria = {}
local fields = parseMatchValue(value)


for i = 1, 10 do
if not fields then


local criterion = trim(args["tie" .. i])
return "'''Ошибка:''' параметр ''" ..
paramName ..
"'' имеет неправильный формат."


if criterion ~= "" then
table.insert(criteria, criterion)
end
end


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


if #criteria == 0 then
end
 
criteria = {
"очки",
"разница",
"забитые",
"победы",
"название"
}


h2hNumber = h2hNumber + 1
end
end




-- -----------------------------------------------------
-- =====================================================
-- Сравнение команд
-- КАРТОЧКИ КОМАНД
-- -----------------------------------------------------
-- =====================================================


-- Возвращает функцию сравнения для table.sort.
-- Параметры yellow_КОД, yellowred_КОД, red_КОД задают
-- teamsMap — команды, среди которых ищутся равные по очкам
-- итог команды. Если указан хотя бы один из них, карточки
-- (для критерия «личные»); lastTour — последний
-- команды берутся только из этих параметров (неуказанные
-- учитываемый тур (nil — все матчи).
-- считаются нулём), а карточки из матчей и стартовых
-- показателей не учитываются.


local function makeComparator(teamsMap, lastTour)
for _, code in ipairs(teamOrder) do


return function(a, b)
local team = teams[code]


for _, criterion in ipairs(criteria) do
local override = {}
local hasOverride = false


-- -----------------------------------------
for _, cardType in ipairs(CARD_TYPES) do
-- ОЧКИ
-- -----------------------------------------


if criterion == "очки" then
local paramName = cardType.key .. "_" .. code
local raw = trim(args[paramName])


if a.points ~= b.points then
if raw ~= "" then
return a.points > b.points
end


if not raw:match("^%d+$") then


-- -----------------------------------------
return "'''Ошибка:''' параметр ''" ..
-- РАЗНИЦА МЯЧЕЙ
paramName ..
-- -----------------------------------------
"'' должен быть целым неотрицательным " ..
"числом, а указано «" .. raw .. "»."


elseif criterion == "разница" then
end


if a.goalDifference ~= b.goalDifference then
override[cardType.key] = tonumber(raw)
return a.goalDifference > b.goalDifference
hasOverride = true
end


end


-- -----------------------------------------
end
-- ЗАБИТЫЕ МЯЧИ
-- -----------------------------------------


elseif criterion == "забитые" then
for _, cardType in ipairs(CARD_TYPES) do


if a.goalsFor ~= b.goalsFor then
if hasOverride then
return a.goalsFor > b.goalsFor
team[cardType.key] =
end
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


elseif criterion == "победы" then


if a.wins ~= b.wins then
-- =====================================================
return a.wins > b.wins
-- РАЗНИЦА МЯЧЕЙ
end
-- =====================================================


for _, code in ipairs(teamOrder) do


-- -----------------------------------------
local team = teams[code]
-- ИГРЫ
 
-- -----------------------------------------
team.goalDifference =
team.goalsFor - team.goalsAgainst


elseif criterion == "игры" then
end


if a.played ~= b.played then
return a.played > b.played
end


-- =====================================================
-- ЛИЧНЫЕ ВСТРЕЧИ
-- =====================================================


-- -----------------------------------------
-- Добавляет сыгранный матч к статистике личных встреч
-- ЛИЧНЫЕ
-- команды code, если обе команды матча входят в группу.
-- -----------------------------------------
local function addHeadToHeadMatch(result, code, group, match)


elseif criterion == "личные" then
if not (group[match.home] and group[match.away]) then
return
end


-- Все команды с таким же
local scored
-- количеством очков, как у a и b.
local conceded


local group = {}
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


for _, code in ipairs(teamOrder) do
result.goalsFor =
result.goalsFor + scored


if teamsMap[code].points == a.points then
result.goalsAgainst =
group[code] = true
result.goalsAgainst + conceded
end


end
if scored > conceded then


local ha = headToHead(a.code, group, lastTour)
result.points =
local hb = headToHead(b.code, group, lastTour)
result.points + pointsWin


if ha.points ~= hb.points then
result.wins =
return ha.points > hb.points
result.wins + 1
end


if ha.goalDifference ~= hb.goalDifference then
elseif scored == conceded then
return ha.goalDifference > hb.goalDifference
end


if ha.goalsFor ~= hb.goalsFor then
result.points =
return ha.goalsFor > hb.goalsFor
result.points + pointsDraw
end


if ha.wins ~= hb.wins then
else
return ha.wins > hb.wins
end


result.points =
result.points + pointsLoss


-- -----------------------------------------
end
-- НАЗВАНИЕ
-- -----------------------------------------


elseif criterion == "название" then
end


local aname =
mw.text.unstrip(a.name)


local bname =
-- Возвращает статистику команды только в матчах
mw.text.unstrip(b.name)
-- против команд из указанной группы.
-- Матчи h2h_matchN учитываются всегда, а матчи турнира —
-- если задан lastTour, только с номером тура не больше lastTour.
-- Снятие очков на личные встречи не влияет.


if aname ~= bname then
local function headToHead(code, group, lastTour)
return aname < bname
end


end
local result = {
points = 0,
goalsFor = 0,
goalsAgainst = 0,
goalDifference = 0,
wins = 0
}


end
-- Матчи, сыгранные до этого турнира
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)


return a.code < b.code
inRange =
end
tourNumber ~= nil and tourNumber <= lastTour
end


end
if match.played and inRange then
addHeadToHeadMatch(result, code, group, match)
end


end


-- =====================================================
result.goalDifference =
-- Выделение победителей и проигравших
result.goalsFor - result.goalsAgainst
 
return result
end
 
 
-- =====================================================
-- КРИТЕРИИ ОПРЕДЕЛЕНИЯ МЕСТ
-- =====================================================
-- =====================================================


-- highlightN = место;количество;класс;текст
-- По умолчанию:
-- 1. очки
-- 2. разница мячей
-- 3. забитые мячи
-- 4. победы
-- 5. название


local highlights = {}
local criteria = {}
 
-- Есть ли текст хотя бы у одного выделения
local hasHighlightText = false


for i = 1, 10 do
for i = 1, 10 do


local hl = trim(args["highlight" .. i])
local criterion = trim(args["tie" .. i])


if hl == "" then
if criterion ~= "" then
break
table.insert(criteria, criterion)
end
end


local parts = split(hl, ";")
end


local place = tonumber(parts[1])
-- Если критерии не указаны


if place then
if #criteria == 0 then


local highlight = {
criteria = {
place = math.floor(place),
"очки",
count = math.max(1, math.floor(tonumber(parts[2]) or 1)),
"разница",
color = parts[3] or "",
"забитые",
-- Текст может сам содержать «;»
"победы",
text = table.concat(parts, "; ", 4)
"название"
}
}


if highlight.text ~= "" then
end
hasHighlightText = true
end


table.insert(highlights, highlight)


end
-- -----------------------------------------------------
-- Сравнение команд
-- -----------------------------------------------------


end
-- Возвращает функцию сравнения для table.sort.
-- teamsMap — команды, среди которых ищутся равные по очкам
-- (для критерия «личные»); lastTour — последний
-- учитываемый тур (nil — все матчи).


table.sort(highlights, function(a, b)
local function makeComparator(teamsMap, lastTour)
return a.place < b.place
end)


return function(a, b)


-- -----------------------------------------------------
for _, criterion in ipairs(criteria) do
-- Режим отображения шахматки
-- -----------------------------------------------------
-- По умолчанию первая строчка - домашние игры, вторая строчка - гостевые
-- Если resultsMode = round, то первая строчка - 1 круг, вторая строчка - 2 круг
local resultsMode = 0
if args["resultsMode"] ~= nil and args["resultsMode"] ~= "" then
if args["resultsMode"] == "round" then
resultsMode = 1
end
end


-- -----------------------------------------
-- ОЧКИ
-- -----------------------------------------


-- =====================================================
if criterion == "очки" then
-- ПРОГРЕСС ПО ТУРАМ
-- =====================================================


local function buildProgress()
if a.points ~= b.points then
return a.points > b.points
end


local progress = {}


-- Собираем список туров
-- -----------------------------------------
local tours = {}
-- РАЗНИЦА МЯЧЕЙ
-- -----------------------------------------


for _, match in ipairs(matches) do
elseif criterion == "разница" then


local tourNumber = tonumber(match.tour)
if a.goalDifference ~= b.goalDifference then
return a.goalDifference > b.goalDifference
end


if tourNumber then
tours[tourNumber] = true
end


end
-- -----------------------------------------
-- ЗАБИТЫЕ МЯЧИ
-- -----------------------------------------


local tourNumbers = {}
elseif criterion == "забитые" then


for tourNumber in pairs(tours) do
if a.goalsFor ~= b.goalsFor then
table.insert(tourNumbers, tourNumber)
return a.goalsFor > b.goalsFor
end
end


table.sort(tourNumbers)


-- -----------------------------------------
-- ПОБЕДЫ
-- -----------------------------------------


-- Таблица, которая последовательно
elseif criterion == "победы" then
-- пересчитывается после каждого тура
local currentTeams = {}


for _, code in ipairs(teamOrder) do
if a.wins ~= b.wins then
currentTeams[code] = newTeam(
return a.wins > b.wins
code,
end
teams[code].name
)
end




for _, tourNumber in ipairs(tourNumbers) do
-- -----------------------------------------
-- ИГРЫ
-- -----------------------------------------


-- Добавляем матчи данного тура
elseif criterion == "игры" then
for _, match in ipairs(matches) do


if tonumber(match.tour) == tourNumber then
if a.played ~= b.played then
 
return a.played > b.played
if match.played then
addResult(currentTeams, match)
end
end


-- Снятие очков действует с этого тура
applyDeductions(currentTeams, match)


end
-- -----------------------------------------
-- ЛИЧНЫЕ
-- -----------------------------------------


end
elseif criterion == "личные" then
 
-- Все команды с таким же
-- количеством очков, как у a и b.


local group = {}


-- Разница мячей
for _, code in ipairs(teamOrder) do
for _, code in ipairs(teamOrder) do


local team = currentTeams[code]
if teamsMap[code].points == a.points then
group[code] = true
end


team.goalDifference =
end
team.goalsFor - team.goalsAgainst


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
local tourStandings = {}
return ha.goalDifference > hb.goalDifference
end


for _, code in ipairs(teamOrder) do
if ha.goalsFor ~= hb.goalsFor then
table.insert(
return ha.goalsFor > hb.goalsFor
tourStandings,
end
currentTeams[code]
)
end


table.sort(
if ha.wins ~= hb.wins then
tourStandings,
return ha.wins > hb.wins
makeComparator(currentTeams, tourNumber)
end
)




progress[tourNumber] = {}
-- -----------------------------------------
-- НАЗВАНИЕ
-- -----------------------------------------


for position, team in ipairs(tourStandings) do
elseif criterion == "название" then


progress[tourNumber][team.code] =
local aname =
position
mw.text.unstrip(a.name)


end
local bname =
mw.text.unstrip(b.name)
 
if aname ~= bname then
return aname < bname
end
 
end
 
end


end


-- Последний резервный критерий —
-- код команды.


return progress, tourNumbers
return a.code < b.code
end


end
end
Строка 1195: Строка 1335:


-- =====================================================
-- =====================================================
-- СОРТИРОВКА
-- НУМЕРАЦИЯ МЕСТ
-- =====================================================
-- =====================================================


local standings = {}
-- first_place — номер первого места таблицы. Например,
-- в таблице группы за 7–14 места первая строка — 7-е место.
-- Места в highlightN и progress_highlightN указываются
-- в этой же нумерации.
 
local placeOffset = 0
 
local firstPlace = trim(args.first_place)


for _, code in ipairs(teamOrder) do
if firstPlace ~= "" then
table.insert(standings, teams[code])
end


table.sort(standings, makeComparator(teams, nil))
if not firstPlace:match("^%d+$") or
  tonumber(firstPlace) < 1 then


local teamCount = #standings
return "'''Ошибка:''' параметр ''first_place'' должен " ..
"быть целым положительным числом, а указано «" ..
firstPlace .. "»."


end


-- =====================================================
placeOffset = tonumber(firstPlace) - 1
-- Подсветка
-- =====================================================


-- Цвет строк
end
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
-- highlightN = место;количество;класс;текст


-- Ячейки столбца «Повышение или выбывание».
local highlights = {}
-- Каждая зона — одна объединённая ячейка с текстом,
-- места между зонами — объединённые пустые ячейки.
if hasHighlightText then


local position = 1
-- Есть ли текст хотя бы у одного выделения
local index = 1
local hasHighlightText = false


while position <= teamCount do
for i = 1, 15 do


-- Пропускаем зоны, которые начинаются выше
local hl = trim(args["highlight" .. i])
-- текущей строки (пересекаются с предыдущей)
while highlights[index] and
      highlights[index].place < position do
index = index + 1
end


local hl = highlights[index]
if hl == "" then
local span
break
local text
end
 
local parts = split(hl, ";")
 
local place = tonumber(parts[1])
 
if place then
 
-- Номер строки таблицы с учётом first_place
local row = math.floor(place) - placeOffset


if hl and hl.place == position then
local count =
math.max(1, math.floor(tonumber(parts[2]) or 1))


span = math.min(hl.count, teamCount - position + 1)
-- Зона, которая начинается выше таблицы,
text = hl.text
-- обрезается по первой строке
index = index + 1
if row < 1 then
count = count + row - 1
row = 1
end


else
-- Зоны целиком за пределами таблицы пропускаются
if count >= 1 and row <= #teamOrder then


local nextPlace = teamCount + 1
local highlight = {
place = row,
count = count,
color = parts[3] or "",
-- Текст может сам содержать «;»
text = table.concat(parts, "; ", 4)
}


if hl then
if highlight.text ~= "" then
nextPlace = math.min(hl.place, teamCount + 1)
hasHighlightText = true
end
end


span = nextPlace - position
table.insert(highlights, highlight)
text = ""


end
end


standings[position].zoneCell = {
text = text,
span = span
}
position = position + span
end
end


end
end
table.sort(highlights, function(a, b)
return a.place < b.place
end)




-- =====================================================
-- -----------------------------------------------------
-- ПРИМЕЧАНИЯ О СНЯТИИ ОЧКОВ
-- Режим отображения шахматки
-- =====================================================
-- -----------------------------------------------------
 
-- По умолчанию первая строчка - домашние игры, вторая строчка - гостевые
-- Текст примечания, например:
-- Если resultsMode = round, то первая строчка - 1 круг, вторая строчка - 2 круг
-- «С команды «Спартак» снято 3 очка в 5-м туре
local resultsMode = 0
-- (матч «Спартак» — «Локомотив») за неявку на матч.»
if args["resultsMode"] ~= nil and args["resultsMode"] ~= "" then
local function deductionText(deduction)
if args["resultsMode"] == "round" then
resultsMode = 1
end
end


local match = deduction.match
-- rounds = 1 — однокруговой турнир: у каждой команды в шахматке
-- одна строка, в ячейке — единственный матч пары (resultsMode
-- при этом не действует). rounds = 2 или пусто — двухкруговой.
local roundsParameter = trim(args.rounds)


local text =
if roundsParameter ~= "" and
"С команды «" .. teams[deduction.team].name ..
  roundsParameter ~= "1" and
"» снято " .. formatNumber(deduction.points) .. " " ..
  roundsParameter ~= "2" then
plural(deduction.points, "очко", "очка", "очков")


local tourNumber = tonumber(match.tour)
return "'''Ошибка:''' параметр ''rounds'' может быть равен " ..
"1 (однокруговой турнир) или 2 (двухкруговой), " ..
"а указано «" .. roundsParameter .. "»."


if tourNumber then
end
text = text .. " в " ..
formatNumber(tourNumber) .. "-м туре"
end


text = text ..
local singleRound = roundsParameter == "1"
" (матч «" .. teams[match.home].name ..
"» — «" .. teams[match.away].name .. "»)"


if deduction.reason ~= "" then
text = text .. " " .. deduction.reason
end


return text .. "."
-- =====================================================
end
-- ПРОГРЕСС ПО ТУРАМ
-- =====================================================


local function buildProgress()


-- Сноски нумеруются сверху вниз по итоговой таблице
local progress = {}
local notes = {}


for _, team in ipairs(standings) do
-- Собираем список туров
local tours = {}


for _, deduction in ipairs(deductions) do
for _, match in ipairs(matches) do


if deduction.team == team.code then
local tourNumber = tonumber(match.tour)


local label = noteLabel(#notes + 1)
if tourNumber then
tours[tourNumber] = true
end


local note = {
end
label = label,
 
id = idPrefix .. "-note-" .. label,
local tourNumbers = {}
refId = idPrefix .. "-ref-" .. label,
 
text = deductionText(deduction)
for tourNumber in pairs(tours) do
}
table.insert(tourNumbers, tourNumber)
end
 
table.sort(tourNumbers)


deduction.note = note


table.insert(notes, note)
-- Таблица, которая последовательно
table.insert(team.notes, note)
-- пересчитывается после каждого тура
local currentTeams = {}


end
for _, code in ipairs(teamOrder) do
currentTeams[code] = newTeam(
code,
teams[code].name
)


-- Стартовые показатели действуют с первого тура
addStart(currentTeams[code], teams[code].start)
end
end


end


for _, tourNumber in ipairs(tourNumbers) do


-- Сноска вида [a]. Сноска в итоговой таблице получает id,
-- Добавляем матчи данного тура
-- чтобы из примечания можно было вернуться к ней (↑).
for _, match in ipairs(matches) do
local function noteMarker(note, withAnchor)


local marker =
if tonumber(match.tour) == tourNumber then
mw.html.create("sup")
:addClass("reference")
:addClass("football-note-ref")
:wikitext(
"[[#" .. note.id .. "|&#91;" ..
note.label .. "&#93;]]"
)


if withAnchor then
if match.played then
marker:attr("id", note.refId)
addResult(currentTeams, match)
end
end


return marker
-- Снятие очков действует с этого тура
end
applyDeductions(currentTeams, match)


end


-- =====================================================
end
-- ТАБЛИЦА ЧЕМПИОНАТА
-- =====================================================


local root =
mw.html.create("div")
:addClass("football-tables")


-- Разница мячей
for _, code in ipairs(teamOrder) do
local team = currentTeams[code]


local standingsTable =
team.goalDifference =
mw.html.create("table")
team.goalsFor - team.goalsAgainst
:addClass("wikitable")
:addClass("football-standings")
:css("text-align", "center")


end


-- Заголовок


local standingsHeader =
-- Таблица после этого тура
mw.html.create("tr")
local tourStandings = {}


local headings = {
for _, code in ipairs(teamOrder) do
"Место",
table.insert(
"Команда",
tourStandings,
"И",
currentTeams[code]
"В",
)
"Н",
end
"П",
 
"МЗ",
table.sort(
"МП",
tourStandings,
"РМ",
makeComparator(currentTeams, tourNumber)
"О"
)
}


for _, title in ipairs(headings) do


standingsHeader:node(
progress[tourNumber] = {}
cell("th", title)
)


end
for position, team in ipairs(tourStandings) do


-- Столбцы карточек выводятся, только если
progress[tourNumber][team.code] =
-- карточки где-нибудь указаны
position
if hasCards then


for _, cardType in ipairs(CARD_TYPES) do
end
 
end


standingsHeader:node(
mw.html.create("th")
:addClass("football-cards")
:attr("title", cardType.title)
:node(cardIcon(cardType))
)


end
return progress, tourNumbers


end
end


if hasHighlightText then


standingsHeader:node(
-- =====================================================
cell("th", "Повышение или выбывание")
-- СОРТИРОВКА
)
-- =====================================================
 
local standings = {}


for _, code in ipairs(teamOrder) do
table.insert(standings, teams[code])
end
end


standingsTable:node(standingsHeader)
table.sort(standings, makeComparator(teams, nil))


local teamCount = #standings


-- Строки


for position, team in ipairs(standings) do
-- =====================================================
-- Подсветка
-- =====================================================


local row =
-- Цвет строк
mw.html.create("tr")
for _, hl in ipairs(highlights) do
 
local first = math.max(hl.place, 1)
local last = math.min(hl.place + hl.count - 1, teamCount)


if team.highlightColor and team.highlightColor ~= "" then
for position = first, last do
row:addClass("highlight"):addClass(team.highlightColor)
standings[position].highlightColor = hl.color
end
end


row:node(
end
cell("td", position)
)


-- Ячейки столбца «Повышение или выбывание».
-- Каждая зона — одна объединённая ячейка с текстом,
-- места между зонами — объединённые пустые ячейки.
if hasHighlightText then


-- Название и сноски о снятии очков
local position = 1
local index = 1


local teamName =
while position <= teamCount do
cell("td", team.name)
:css("text-align", "left")


for _, note in ipairs(team.notes) do
-- Пропускаем зоны, которые начинаются выше
teamName:node(
-- текущей строки (пересекаются с предыдущей)
noteMarker(note, true)
while highlights[index] and
)
      highlights[index].place < position do
end
index = index + 1
end


row:node(
local hl = highlights[index]
teamName
local span
)
local text


if hl and hl.place == position then


row:node(
span = math.min(hl.count, teamCount - position + 1)
cell("td", team.played)
text = hl.text
)
index = index + 1


row:node(
else
cell("td", team.wins)
)


row:node(
local nextPlace = teamCount + 1
cell("td", team.draws)
)


row:node(
if hl then
cell("td", team.losses)
nextPlace = math.min(hl.place, teamCount + 1)
)
end


row:node(
span = nextPlace - position
cell("td", team.goalsFor)
text = ""
)


row:node(
end
cell("td", team.goalsAgainst)
)


standings[position].zoneCell = {
text = text,
span = span
}


local gd
position = position + span
 
if team.goalDifference > 0 then
gd = "+" .. team.goalDifference
else
gd = tostring(team.goalDifference)
end
end


row:node(
end
cell("td", gd)
)




local pointsCell =
-- =====================================================
cell("td", formatNumber(team.points))
-- ПРИМЕЧАНИЯ О СНЯТИИ ОЧКОВ
-- =====================================================


if team.deducted > 0 then
-- Текст примечания, например:
-- «С команды «Спартак» снято 3 очка в 5-м туре
-- (матч «Спартак» — «Локомотив») за неявку на матч.»
local function deductionText(deduction)


pointsCell:attr(
local match = deduction.match
"title",
"Снято " .. formatNumber(team.deducted) .. " " ..
plural(team.deducted, "очко", "очка", "очков")
)


end
local text =
"С команды «" .. teams[deduction.team].name ..
"» снято " .. formatNumber(deduction.points) .. " " ..
plural(deduction.points, "очко", "очка", "очков")


row:node(
local tourNumber = tonumber(match.tour)
pointsCell
)


if tourNumber then
text = text .. " в " ..
formatNumber(tourNumber) .. "-м туре"
end


if hasCards then
text = text ..
" (матч «" .. teams[match.home].name ..
"» — «" .. teams[match.away].name .. "»)"


for _, cardType in ipairs(CARD_TYPES) do
if deduction.reason ~= "" then
text = text .. " " .. deduction.reason
end


row:node(
return text .. "."
cell("td", team[cardType.key], "football-cards")
end
)


end


end
-- Сноски нумеруются сверху вниз по итоговой таблице
local notes = {}


for _, team in ipairs(standings) do


if hasHighlightText and team.zoneCell then
for _, deduction in ipairs(deductions) do


local zone =
if deduction.team == team.code then
cell("td", team.zoneCell.text)


if team.zoneCell.span > 1 then
local label = noteLabel(#notes + 1)
zone:attr("rowspan", team.zoneCell.span)
end


row:node(
local note = {
zone
label = label,
)
id = idPrefix .. "-note-" .. label,
refId = idPrefix .. "-ref-" .. label,
text = deductionText(deduction)
}
 
deduction.note = note


end
table.insert(notes, note)
table.insert(team.notes, note)


end


standingsTable:node(row)
end


end
end




root:node(standingsTable)
-- Сноска вида [a]. Сноска в итоговой таблице получает id,
-- чтобы из примечания можно было вернуться к ней (↑).
local function noteMarker(note, withAnchor)


local marker =
mw.html.create("sup")
:addClass("reference")
:addClass("football-note-ref")
:wikitext(
"[[#" .. note.id .. "|&#91;" ..
note.label .. "&#93;]]"
)


-- =====================================================
if withAnchor then
-- ИНФОРМАЦИЯ О КРИТЕРИЯХ
marker:attr("id", note.refId)
-- =====================================================
end


local criteriaText = table.concat(criteria, " → ")
return marker
 
end
local info =
mw.html.create("div")
:addClass("football-info")
:wikitext(
"Критерии определения мест: " ..
criteriaText
)
 
root:node(info)




-- =====================================================
-- =====================================================
-- ПРИМЕЧАНИЯ ПОД ТАБЛИЦЕЙ
-- ТАБЛИЦА ЧЕМПИОНАТА
-- =====================================================
-- =====================================================


if #notes > 0 then
local root =
mw.html.create("div")
:addClass("football-tables")


local notesBlock =
mw.html.create("div")
:addClass("football-info")
:addClass("football-notes")
:wikitext("Примечания:")


local list =
local standingsTable =
notesBlock:tag("ol")
mw.html.create("table")
:css("list-style-type", "lower-alpha")
:addClass("wikitable")
:addClass("football-standings")
:css("text-align", "center")


for _, note in ipairs(notes) do


local item =
-- Заголовок
list:tag("li")
:attr("id", note.id)


item:tag("span")
local standingsHeader =
:addClass("mw-cite-backlink")
mw.html.create("tr")
:wikitext("[[#" .. note.refId .. "|↑]]")


item:wikitext(" ")
local headings = {
"Место",
"Команда",
"И",
"В",
"Н",
"П",
"МЗ",
"МП",
"РМ",
"О"
}


item:tag("span")
for _, title in ipairs(headings) do
:addClass("reference-text")
:wikitext(note.text)


end
standingsHeader:node(
cell("th", title)
)


root:node(notesBlock)
end


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))
)


root:wikitext(heading("Результаты"))
end


end


local resultsTable =
if hasHighlightText then
mw.html.create("table")
:addClass("wikitable")
:addClass("football-results")
:css("text-align", "center")


standingsHeader:node(
cell("th", "Повышение или выбывание")
)


-- Заголовок
end


local header =
standingsTable:node(standingsHeader)
mw.html.create("tr")


header:node(
cell("th", "", "football-corner")
)


for _, code in ipairs(teamOrder) do
-- Строки


header:node(
for position, team in ipairs(standings) do
cell(
"th",
teams[code].name,
"football-team-header"
)
)


end
local row =
mw.html.create("tr")


resultsTable:node(header)
if team.highlightColor and team.highlightColor ~= "" then
row:addClass("highlight"):addClass(team.highlightColor)
end


row:node(
cell("td", position + placeOffset)
)


-- Матрица результатов


for _, homeCode in ipairs(teamOrder) do
-- Название и сноски о снятии очков


local homeRow =
local teamName =
mw.html.create("tr")
cell("td", team.name)
:css("text-align", "left")


local awayRow =
for _, note in ipairs(team.notes) do
mw.html.create("tr")
teamName:node(
 
noteMarker(note, true)
local teamHeader =
cell(
"th",
teams[homeCode].name,
"football-team-header"
)
)
teamHeader:attr("rowspan", 2)
end
teamHeader:css("text-align", "right")


homeRow:node(teamHeader)
row:node(
teamName
)




for _, awayCode in ipairs(teamOrder) do
row:node(
cell("td", team.played)
)


if homeCode == awayCode then
row:node(
local diagonal = cell("td", "—", "football-diagonal")
cell("td", team.wins)
diagonal:attr("rowspan", 2)
)
homeRow:node(
diagonal
)


else
row:node(
cell("td", team.draws)
)


local homeResultCell =
row:node(
mw.html.create("td")
cell("td", team.losses)
local awayResultCell =
)
mw.html.create("td")


local homeFound = false
row:node(
local awayFound = false
cell("td", team.goalsFor)
)


-- Нужно ли поменять местами гостевую и домашнюю игры
row:node(
local changeOrder = false
cell("td", team.goalsAgainst)
-- Ищем ВСЕ матчи между командами
)
for _, match in ipairs(matches) do


if match.home == homeCode and
  match.away == awayCode then


homeFound = true
local gd


local text
if team.goalDifference > 0 then
 
gd = "+" .. team.goalDifference
if match.played then
else
text = match.score
gd = tostring(team.goalDifference)
if match.homeGoals > match.awayGoals then
end
homeResultCell
:addClass("ui")
:addClass("green")
elseif match.homeGoals < match.awayGoals then
homeResultCell
:addClass("ui")
:addClass("red")
else
homeResultCell
:addClass("ui")
:addClass("yellow")
end
elseif match.status ~= "" then
text = match.status
else
text = "—"
end


homeResultCell
row:node(
:wikitext(text)
cell("td", gd)
:done()
)


-- Если режим отображения по кругам, и игра была во втором круге, то меняем местами гостевую и домашнюю
if resultsMode == 1 and match.round == "2" then
changeOrder = true
end


end
local pointsCell =
cell("td", formatNumber(team.points))


if match.home == awayCode and
if team.deducted > 0 then
  match.away == homeCode then


awayFound = true
pointsCell:attr(
"title",
"Снято " .. formatNumber(team.deducted) .. " " ..
plural(team.deducted, "очко", "очка", "очков")
)


local text
end


if match.played then
row:node(
text = match.score
pointsCell
if match.homeGoals < match.awayGoals then
)
awayResultCell
:addClass("ui")
:addClass("green")
elseif match.homeGoals > match.awayGoals then
awayResultCell
:addClass("ui")
:addClass("red")
else
awayResultCell
:addClass("ui")
:addClass("yellow")
end
elseif match.status ~= "" then
text = match.status
else
text = "—"
end


awayResultCell
:wikitext(text)
:done()


-- Если режим отображения по кругам, и игра была в первом круге, то меняем местами гостевую и домашнюю
if hasCards then
if resultsMode == 1 and match.round == "1" then
changeOrder = true
end
end


if homeFound and awayFound then
for _, cardType in ipairs(CARD_TYPES) do
break
end
end


row:node(
cell("td", team[cardType.key], "football-cards")
)
end
end


if not homeFound then
homeResultCell:wikitext("—")
end
if not awayFound then
awayResultCell:wikitext("—")
end


if changeOrder then
if hasHighlightText and team.zoneCell then
homeRow:node(awayResultCell)
 
awayRow:node(homeResultCell)
local zone =
else
cell("td", team.zoneCell.text)
homeRow:node(homeResultCell)
 
awayRow:node(awayResultCell)
if team.zoneCell.span > 1 then
end
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
Строка 1801: Строка 2203:


resultsTable:node(homeRow)
resultsTable:node(homeRow)
resultsTable:node(awayRow)
 
if rowsPerTeam > 1 then
resultsTable:node(awayRow)
end


end
end
Строка 1862: Строка 2267:
local progressHighlights = {}
local progressHighlights = {}


for i = 1, 10 do
for i = 1, 15 do


local value =
local value =
Строка 1974: Строка 2379:
for _, tourNumber in ipairs(tourNumbers) do
for _, tourNumber in ipairs(tourNumbers) do


-- Место с учётом first_place
local position =
local position =
progress[tourNumber][code]
progress[tourNumber][code] + placeOffset


local resultCell =
local resultCell =