Module:Build bracket/Logic
Appearance
local Logic = {}
-- stdlib aliases
local m_max = math.max
local m_ceil = math.ceil
local t_insert = table.insert
local str_find = string.find
-- upvalues bound per call
local state, config, Helpers, StateChecks
local isempty, notempty, teamLegs
-- internal binder used by the public fns below
local function bind(_state, _config, _Helpers, _StateChecks)
state, config, Helpers, StateChecks = _state, _config, _Helpers, _StateChecks
isempty = _Helpers and _Helpers.isempty or function(v) return v == nil or v == '' end
notempty = _Helpers and _Helpers.notempty or function(v) return v ~= nil and v ~= '' end
teamLegs = _StateChecks and _StateChecks.teamLegs
end
-- -------------------------
-- Group teams within a round
-- -------------------------
local function _matchGroups()
for j = config.minc, config.c do
state.matchgroup[j] = {}
local mgj = state.matchgroup[j]
local tpm = tonumber(state.teamsPerMatch[j]) or 2
if tpm < 1 then tpm = 2 end
local col = state.entries[j]
if col then
for i = 1, config.r do
local e = col[i]
if e and e.ctype == 'team' then
local idx = tonumber(e.index) or tonumber(e.altindex) or i
local g = m_ceil(idx / tpm)
mgj[i] = g
e.group = g
end
end
end
end
end
-- ----------------------------------------
-- Compute aggregates (score/legs/sets modes)
-- ----------------------------------------
local function _computeAggregate()
if config.aggregate_mode == 'off' or config.aggregate_mode == 'manual' then return end
local modeLow = (config.boldwinner_mode == 'low')
local function numlead(s)
if not s or s == '' then return nil end
return tonumber((s):match('^%d+')) -- leading integer only
end
-- Build groups: round j -> groupId -> {team indices}
local function buildGroupsForRound(j)
local groups = {}
local mg = state.matchgroup[j] or {}
for i = 1, config.r do
local e = state.entries[j][i]
if e and e.ctype == 'team' then
local gid = mg[i]
if gid ~= nil then
local t = groups[gid]; if not t then t = {}; groups[gid] = t end
t[#t+1] = i
end
end
end
return groups
end
-- Pre-parse leg numbers for each team (per round)
local function preparseLegs(j)
local legNums = {} -- i -> { [l] = number|nil }
for i = 1, config.r do
local e = state.entries[j][i]
if e and e.ctype == 'team' then
local L = teamLegs(j, i)
if config.aggregate and L > 1 and (e.score and e.score.agg ~= nil) then
legNums[i] = {}
for l = 1, L do
legNums[i][l] = numlead(e.score[l])
end
end
end
end
return legNums
end
for j = config.minc, config.c do
local groups = buildGroupsForRound(j)
local legNums = preparseLegs(j)
if config.aggregate_mode == 'score' then
-- Sum per-leg scores
for _, members in pairs(groups) do
for _, i in ipairs(members) do
local e = state.entries[j][i]
if e and e.ctype == 'team' and config.aggregate and teamLegs(j, i) > 1 then
if isempty(e.score.agg) then
local sum = 0
local nums = legNums[i]
if nums then
for _, v in ipairs(nums) do if v then sum = sum + v end end
e.score.agg = tostring(sum)
end
end
end
end
end
else
-- 'sets'/'legs' → count leg wins using high/low rule; ties = no win; non-numeric leg => skip
for _, members in pairs(groups) do
local wins = {} -- per-team index
-- find number of comparable legs across the group
local commonLegs = math.huge
for _, i in ipairs(members) do
local nums = legNums[i]
local L = (nums and #nums) or 0
if L == 0 then commonLegs = 0; break end
if L < commonLegs then commonLegs = L end
end
for l = 1, commonLegs do
-- all teams must have a numeric value for this leg
local allNumeric = true
for _, i in ipairs(members) do
if not (legNums[i] and legNums[i][l] ~= nil) then
allNumeric = false; break
end
end
if allNumeric then
local best, bestIndex, tie = nil, nil, false
for _, i in ipairs(members) do
local v = legNums[i][l]
if best == nil then
best, bestIndex, tie = v, i, false
else
if (modeLow and v < best) or (not modeLow and v > best) then
best, bestIndex, tie = v, i, false
elseif v == best then
tie = true
end
end
end
if not tie and bestIndex then
wins[bestIndex] = (wins[bestIndex] or 0) + 1
end
end
end
-- Write aggregates if still empty
for _, i in ipairs(members) do
local e = state.entries[j][i]
if e and e.ctype == 'team' and config.aggregate and teamLegs(j, i) > 1 then
if isempty(e.score.agg) then
e.score.agg = tostring(wins[i] or 0)
end
end
end
end
end
end
end
-- ---------------------
-- Bold winners (cells & rows)
-- ---------------------
local function _boldWinner()
local modeLow = (config.boldwinner_mode == 'low')
local aggOnly = config.boldwinner_aggonly
local function isWin(mine, theirs)
if modeLow then return mine < theirs else return mine > theirs end
end
local function isAggWin(mine, theirs, l)
if l == 'agg' and config.aggregate_mode == 'sets' then
-- Sets/legs won: larger count wins regardless of low/high scoring sport
return mine > theirs
else
return isWin(mine, theirs)
end
end
local function boldScore(j, i, l)
local e = state.entries[j][i]
if not e or e.ctype ~= 'team' then return 'normal' end
local raw = e.score[l] or ''
local mine = tonumber((raw):match('^%d+'))
if not mine then return 'normal' end
local comps = {}
for oppIndex, groupId in pairs(state.matchgroup[j]) do
if groupId == state.matchgroup[j][i] and oppIndex ~= i then
local theirraw = state.entries[j][oppIndex].score[l] or ''
local theirs = tonumber((theirraw):match('^%d+'))
if not theirs then return 'normal' end
t_insert(comps, theirs)
end
end
for _, v in ipairs(comps) do
if not isAggWin(mine, v, l) then return 'normal' end
end
if l ~= 'agg' then
e.wins = (e.wins or 0) + 1
else
e.aggwins = 1
end
return 'bold'
end
local function boldTeam(j, i, useAggregate)
local e = state.entries[j][i]
local winsKey = useAggregate and 'aggwins' or 'wins'
local legs = teamLegs(j, i)
if not useAggregate then
if (e[winsKey] or 0) > legs / 2 then return 'bold' end
local checkFn = config.autolegs and notempty or function(val) return not isempty(val) end
for l = 1, legs do
local sv = e.score[l]
if not checkFn(sv) or str_find(sv or '', "nbsp") then
return 'normal'
end
end
end
for oppIndex, groupId in pairs(state.matchgroup[j]) do
if groupId == state.matchgroup[j][i] and oppIndex ~= i then
if (e[winsKey] or 0) <= tonumber(state.entries[j][oppIndex][winsKey] or 0) then
return 'normal'
end
end
end
return 'bold'
end
-- reset counters
for j = config.minc, config.c do
for i = 1, config.r do
if state.entries[j][i] and state.entries[j][i].ctype == 'team' then
state.entries[j][i].wins = 0
state.entries[j][i].aggwins = 0
end
end
-- per-score bolding
for i = 1, config.r do
if state.entries[j][i] and state.entries[j][i].ctype == 'team' then
local legs = teamLegs(j, i)
-- legs (skip entirely if agg-only)
if not aggOnly then
for l = 1, legs do
state.entries[j][i].score.weight[l] = boldScore(j, i, l)
end
end
-- aggregate column (if present)
if config.aggregate and legs > 1 then
state.entries[j][i].score.weight.agg = boldScore(j, i, 'agg')
end
end
end
-- whole-team bolding (skip if agg-only so only agg cell bolds)
if not aggOnly then
for i = 1, config.r do
if state.entries[j][i] and state.entries[j][i].ctype == 'team' then
local useAggregate = config.aggregate and teamLegs(j, i) > 1
state.entries[j][i].weight = boldTeam(j, i, useAggregate)
end
end
end
end
end
-- ---------------------
-- Update per-round max legs
-- ---------------------
local function _updateMaxLegs()
for j = config.minc, config.c do
state.maxlegs[j] = state.rlegs[j]
for i = 1, config.r do
if notempty(state.entries[j][i]) then
if notempty(state.entries[j][i].legs) then
state.maxlegs[j] = m_max(state.rlegs[j], state.entries[j][i].legs)
end
if config.autolegs then
local l = 1
repeat l = l + 1
until isempty(state.entries[j][i].score) or isempty(state.entries[j][i].score[l])
state.maxlegs[j] = m_max(state.maxlegs[j], l - 1)
end
end
end
end
end
-- -------------
-- Public API
-- -------------
function Logic.matchGroups(_state, _config)
bind(_state, _config)
_matchGroups()
end
function Logic.computeAggregate(_state, _config, _Helpers, _StateChecks)
bind(_state, _config, _Helpers, _StateChecks)
_computeAggregate()
end
function Logic.boldWinner(_state, _config, _Helpers, _StateChecks)
bind(_state, _config, _Helpers, _StateChecks)
_boldWinner()
end
function Logic.updateMaxLegs(_state, _config, _Helpers)
bind(_state, _config, _Helpers)
_updateMaxLegs()
end
return Logic