Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

Jump to content

Module:Build bracket/Logic

Permanently protected module
From Wikipedia, the free encyclopedia
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