Module:Build bracket/Logic/sandbox

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

-- Numeric tiebreaker inside parentheses, e.g., "2 (3)" -> 3; nil if none.
local function parenTie(s)
    if s == nil then return nil end
    local str = tostring(s)
    local last
    for m in str:gmatch("%(([%d%.]+)%)") do
        last = tonumber(m)
    end
    return last
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 bestMain, bestIdx, bestTB, tie = nil, nil, nil, false
						for _, i in ipairs(members) do
						    local v = legNums[i][l]
						    local raw = state.entries[j][i].score and state.entries[j][i].score[l]
						    local tb  = parenTie(raw) or 0
						
						    if bestMain == nil then
						        bestMain, bestIdx, bestTB, tie = v, i, tb, false
						    else
						        if (modeLow and v < bestMain) or (not modeLow and v > bestMain) then
						            bestMain, bestIdx, bestTB, tie = v, i, tb, false
						        elseif v == bestMain then
						            if (modeLow and tb < bestTB) or (not modeLow and tb > bestTB) then
						                bestMain, bestIdx, bestTB, tie = v, i, tb, false
						            elseif tb == bestTB then
						                tie = true
						            end
						        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 bestMain, bestIdx, bestTB, tie = nil, nil, nil, false
				
				    for _, i in ipairs(members) do
				        local v = perLegNum[i] and perLegNum[i][l]
				        if v == nil then
				            allNumeric = false
				            break
				        end
				
				        -- paren tiebreak (treat missing as 0 so "2 (3)" beats "2")
				        local raw = state.entries[j][i].score and state.entries[j][i].score[l]
				        local tb  = parenTie(raw) or 0
				
				        if bestMain == nil then
				            bestMain, bestIdx, bestTB, tie = v, i, tb, false
				        else
				            if (modeLow and v < bestMain) or (not modeLow and v > bestMain) then
				                bestMain, bestIdx, bestTB, tie = v, i, tb, false
				            elseif v == bestMain then
				                if (modeLow and tb < bestTB) or (not modeLow and tb > bestTB) then
				                    bestMain, bestIdx, bestTB, tie = v, i, tb, false
				                elseif tb == bestTB then
				                    tie = true
				                end
				            end
				        end
				    end
				
				    if allNumeric and not tie and bestIdx then
				        local e = state.entries[j][bestIdx]
				        e.score.weight[l] = "bold"
				        e.wins = (e.wins or 0) + 1
				        for _, i in ipairs(members) do
				            if i ~= bestIdx then
				                state.entries[j][i].score.weight[l] = "normal"
				            end
				        end
				    else
				        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

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.