Przejdź do zawartości

Moduł:Build bracket/Logic

Z Wikipedii, wolnej encyklopedii
 Dokumentacja modułu[stwórz] • [odśwież]
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) -- usunięcie ( )
            if s2 ~= s then
                s, changed = s2, true
            end
            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