Module:Build bracket/Logic/sandbox
| This is the module sandbox page for Module:Build bracket/Logic (diff). |
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.
- 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:
- 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.
- 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.
- 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.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.