Moduł:Build bracket/Logic
Wygląd
local Logic = {}
-- =======================
-- 1) STDLIB ALIASES
-- =======================
local m_max = math.max
local m_ceil = math.ceil
local s_match = string.match
local s_find = string.find
-- =======================
-- 2) MODULE UPVALUES
-- =======================
local state, config, Helpers, StateChecks
local isempty, notempty, teamLegs
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
-- =======================
-- 3) SMALL UTILITIES
-- =======================
-- Map common Unicode fraction characters to decimal
local fraction_map = {
["½"] = ".5",
["⅓"] = ".333",
["⅔"] = ".667",
["¼"] = ".25",
["¾"] = ".75",
["⅕"] = ".2",
["⅖"] = ".4",
["⅗"] = ".6",
["⅘"] = ".8",
["⅙"] = ".167",
["⅚"] = ".833",
["⅛"] = ".125",
["⅜"] = ".375",
["⅝"] = ".625",
["⅞"] = ".875"
}
-- Normalize fractions like "1½" to "1.5"
local function normalizeFractions(s)
-- Replace integer + fraction (e.g. "1½" → "1.5")
s =
s:gsub(
"(%d+)%s*([%z\1-\127\194-\244][\128-\191]*)",
function(d, frac)
local repl = fraction_map[frac]
if repl then
return d .. repl
else
return d .. frac
end
end
)
-- Replace standalone fraction (e.g. "½" → "0.5")
s =
s:gsub(
"([%z\1-\127\194-\244][\128-\191]*)",
function(frac)
local repl = fraction_map[frac]
if repl then
return "0" .. repl
else
return frac
end
end
)
return s
end
-- Leading integer from a value; supports string or number; nil if none.
local function numlead(v)
if v == nil then
return nil
end
if type(v) == "number" then
return v
end
local s = tostring(v)
if s == "" then
return nil
end
-- 1) strip leading spaces and wiki bold/italic quotes ('' or ''')
s = s:match("^%s*'*%s*(.*)") or s
-- 2) strip *leading* simple wrappers (tags/templates/links), repeatedly, but only if they occur before the first digit.
local advanced = true -- set false if you want the minimal version
if advanced then
local changed = true
while changed do
changed = false
local s2 = s:gsub("^%s*<[^>]->%s*", "", 1) -- leading HTML tag
if s2 ~= s then
s, changed = s2, true
end
s2 = s:gsub("^%s*{{.-}}%s*", "", 1) -- leading template
if s2 ~= s then
s, changed = s2, true
end
s2 = s:gsub("^%s*%[%[[^%]]-%]%]%s*", "", 1) -- leading wikilink
if s2 ~= s then
s, changed = s2, true
end
end
end
-- 3) capture leading digits only (stops at first non-digit)
s = normalizeFractions(s)
local n = s:match("^(%d+%.?%d*)")
return n and tonumber(n) or nil
end
-- ===========================
-- 4) MATCH GROUPING (PER RD)
-- ===========================
local function _matchGroups()
state.matchgroup = state.matchgroup or {}
local MINC, C, R = config.minc, config.c, config.r
for j = MINC, C do
local mgj = {}
state.matchgroup[j] = mgj
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, 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
-- Build ordered lists once per round: gid -> {row indices}
local function buildGroupsForRound(j, R)
local groups = {}
local mg = (state.matchgroup and state.matchgroup[j]) or {}
local col = state.entries[j]
if not col then
return groups
end
for i = 1, R do
local e = col[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), only when agg & >1 legs.
-- Returns: i -> { [l] = number|nil }
local function preparseLegs(j, R)
local legNums = {}
local col = state.entries[j]
if not col then
return legNums
end
for i = 1, R do
local e = col[i]
if e and e.ctype == "team" then
local L = teamLegs(j, i)
if config.aggregate and L > 1 and e.score then
local t = {}
for l = 1, L do
t[l] = numlead(e.score[l])
end
legNums[i] = t
end
end
end
return legNums
end
-- ==========================================
-- 5) COMPUTE AGGREGATES (score/legs/sets)
-- ==========================================
local function _computeAggregate()
if config.aggregate_mode == "off" or config.aggregate_mode == "manual" then
return
end
local MINC, C, R = config.minc, config.c, config.r
local modeLow = (config.boldwinner_mode == "low")
for j = MINC, C do
local groups = buildGroupsForRound(j, R)
local legNums = preparseLegs(j, R)
if config.aggregate_mode == "score" then
-- Sum per-leg scores; operate directly on parsed legNums.
for i, nums in pairs(legNums) do
local e = state.entries[j][i]
if e and e.ctype == "team" and config.aggregate and teamLegs(j, i) > 1 then
local sc = e.score
if sc and isempty(sc.agg) then
local sum = 0
for _, v in ipairs(nums) do
if v then
sum = sum + v
end
end
sc.agg = tostring(sum)
end
end
end
else
-- 'sets'/'legs': count wins per-leg using high/low rule; ties yield no win.
for _, members in pairs(groups) do
local wins = {} -- row index -> wins
-- Comparable leg count across the group = min(#numeric arrays)
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
local allNumeric = true
for _, i in ipairs(members) do
local v = legNums[i] and legNums[i][l]
if v == nil then
allNumeric = false
break
end
end
if allNumeric then
local best, bestIdx, tie
for _, i in ipairs(members) do
local v = legNums[i][l]
if best == nil then
best, bestIdx, tie = v, i, false
else
if (modeLow and v < best) or (not modeLow and v > best) then
best, bestIdx, tie = v, i, false
elseif v == best then
tie = true
end
end
end
if not tie and bestIdx then
wins[bestIdx] = (wins[bestIdx] 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
local sc = e.score
if sc and isempty(sc.agg) then
sc.agg = tostring(wins[i] or 0)
end
end
end
end
end
end
end
-- ==========================================
-- 6) BOLD WINNERS (per cells & whole rows)
-- ==========================================
local function _boldWinner()
local function isWin(mine, theirs)
return modeLow and (mine < theirs) or (not modeLow and mine > theirs)
end
local function isAggWin(mine, theirs, colKey)
-- For aggregate counts (legs/sets won), higher is always better
if colKey == "agg" and (config.aggregate_mode ~= "score") then
return mine > theirs
end
return isWin(mine, theirs)
end
if not config or config.boldwinner_mode == "off" then
-- Normalize all weights (defensive; avoids leftovers across calls)
for j = config.minc, config.c do
for i = 1, config.r do
local e = state.entries[j] and state.entries[j][i]
if e and e.ctype == "team" then
e.weight = "normal"
if e.score and e.score.weight then
for k, _ in pairs(e.score.weight) do
e.score.weight[k] = "normal"
end
end
end
end
end
return
end
local MINC, C, R = config.minc, config.c, config.r
local modeLow = (config.boldwinner_mode == "low")
local aggOnly = config.boldwinner_aggonly
local function isWin(mine, theirs)
return modeLow and (mine < theirs) or (not modeLow and mine > theirs)
end
local function isAggWin(mine, theirs, colKey)
if colKey == "agg" and config.aggregate_mode == "sets" then
-- Sets/legs won: larger count wins regardless of low/high sport
return mine > theirs
end
return isWin(mine, theirs)
end
local function hasAllScores(e, legs)
local sc = e.score
for l = 1, legs do
local sv = sc and sc[l]
if isempty(sv) or s_find(sv or "", "nbsp") then -- use alias
return false
end
end
return true
end
for j = MINC, C do
local groups = buildGroupsForRound(j, R)
-- Reset counters & ensure score/weight tables exist
for i = 1, R do
local e = state.entries[j] and state.entries[j][i]
if e and e.ctype == "team" then
e.wins, e.aggwins = 0, 0
e.score = e.score or {}
e.score.weight = e.score.weight or {}
end
end
for _, members in pairs(groups) do
-- Parse per-leg and aggregate numbers ONCE for this group (works for 1+ legs)
local perLegNum = {} -- row -> { l -> number|nil }
local aggNum = {} -- row -> number|nil
for _, i in ipairs(members) do
local e = state.entries[j][i]
local legs = teamLegs(j, i)
local arr = {}
for l = 1, legs do
arr[l] = numlead(e.score and e.score[l])
end
perLegNum[i] = arr
aggNum[i] = numlead(e.score and e.score.agg)
end
-- Per-score bolding (skip entirely if agg-only)
if not aggOnly then
-- iterate to the max leg count in the group
local maxL = 0
for _, i in ipairs(members) do
local L = teamLegs(j, i)
if L > maxL then
maxL = L
end
end
for l = 1, maxL do
local allNumeric = true
local best, bestIdx, tie
for _, i in ipairs(members) do
local v = perLegNum[i] and perLegNum[i][l]
if v == nil then
allNumeric = false
break
end
if best == nil then
best, bestIdx, tie = v, i, false
else
if (modeLow and v < best) or (not modeLow and v > best) then
best, bestIdx, tie = v, i, false
elseif v == best then
tie = true
end
end
end
if allNumeric and not tie and bestIdx then
-- set the winner bold + increment wins
local e = state.entries[j][bestIdx]
e.score.weight[l] = "bold"
e.wins = (e.wins or 0) + 1
-- normalize others on this leg
for _, i in ipairs(members) do
if i ~= bestIdx then
state.entries[j][i].score.weight[l] = "normal"
end
end
else
-- no unique winner: normalize everyone for this leg
for _, i in ipairs(members) do
state.entries[j][i].score.weight[l] = "normal"
end
end
end
end
-- Aggregate column (if configured & multi-leg)
do
local needAgg = false
for _, i in ipairs(members) do
if config.aggregate and teamLegs(j, i) > 1 then
needAgg = true
break
end
end
if needAgg then
local allNumeric = true
local best, bestIdx, tie
-- comparator: for legs/sets counts, higher always wins
local function betterAgg(a, b)
if config.aggregate_mode ~= "score" then
return a > b
else
return (modeLow and a < b) or (not modeLow and a > b)
end
end
for _, i in ipairs(members) do
local v = aggNum[i]
if v == nil then
allNumeric = false
break
end
if best == nil then
best, bestIdx, tie = v, i, false
else
if betterAgg(v, best) then
best, bestIdx, tie = v, i, false
elseif v == best then
tie = true
end
end
end
if allNumeric and not tie and bestIdx then
local e = state.entries[j][bestIdx]
e.score.weight.agg = "bold"
e.aggwins = 1
for _, i in ipairs(members) do
if i ~= bestIdx then
local o = state.entries[j][i]
o.score.weight.agg = "normal"
o.aggwins = 0
end
end
else
for _, i in ipairs(members) do
local e = state.entries[j][i]
if e.score then
e.score.weight.agg = "normal"
end
e.aggwins = 0
end
end
end
end
-- Whole-team bolding (skip if agg-only so only agg cell bolds)
if not aggOnly then
for _, i in ipairs(members) do
local e = state.entries[j][i]
local legs = teamLegs(j, i)
local useAggregate = config.aggregate and legs > 1
local winsKey = useAggregate and "aggwins" or "wins"
if not useAggregate then
if (e[winsKey] or 0) > legs / 2 then
e.weight = "bold"
else
e.weight = hasAllScores(e, legs) and "bold" or "normal"
end
end
-- Must strictly beat any opponent on winsKey
for _, oi in ipairs(members) do
if oi ~= i then
local opp = state.entries[j][oi]
if (e[winsKey] or 0) <= tonumber(opp[winsKey] or 0) then
e.weight = "normal"
break
end
end
end
if useAggregate then
-- when using aggregate, team weight follows aggwins comparison
e.weight = ((e[winsKey] or 0) > 0) and "bold" or "normal"
for _, oi in ipairs(members) do
if oi ~= i then
local opp = state.entries[j][oi]
if (e[winsKey] or 0) <= tonumber(opp[winsKey] or 0) then
e.weight = "normal"
break
end
end
end
end
end
end
end
end
end
-- ==============================
-- 7) UPDATE PER-ROUND MAX LEGS
-- ==============================
local function _updateMaxLegs()
local MINC, C, R = config.minc, config.c, config.r
for j = MINC, C do
local col = state.entries[j]
local rj = state.rlegs[j]
local mj = rj
if col then
for i = 1, R do
local e = col[i]
if e then
if notempty(e.legs) then
mj = m_max(rj, e.legs)
end
if config.autolegs and e.score then
local l = 1
while e.score[l] and not isempty(e.score[l]) do
l = l + 1
end
mj = m_max(mj, l - 1)
end
end
end
end
state.maxlegs[j] = mj
end
end
-- ==============
-- 8) 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