A friend of mine runs a small amateur league here in Namangan and keeps the table in a spreadsheet. Last month two teams finished on the same points and he sorted them by hand. I wrote him a short script so the tie-break is applied the same way every time.
The rule he uses: points first, then the head-to-head record between the tied teams, then goal difference, then goals scored. The trap is that head-to-head only makes sense among the teams that are actually tied, so you cannot put it into one flat comparator.
Here is the approach that worked. First sort by points. Then walk through the sorted list and cut it into groups of equal points. Inside each group of two or more, build a small table from only the matches played between those teams, and sort the group by that mini table, falling back to overall goal difference and goals scored.
function sortTable(teams, matches) {
const byPoints = [...teams].sort((a, b) => b.pts - a.pts);
const groups = [];
for (const t of byPoints) {
const last = groups[groups.length - 1];
if (last && last[0].pts === t.pts) last.push(t);
else groups.push([t]);
}
return groups.flatMap(g => (g.length === 1 ? g : breakTie(g, matches)));
}
function breakTie(group, matches) {
const names = new Set(group.map(t => t.name));
const mini = Object.fromEntries(group.map(t => [t.name, { pts: 0, gd: 0 }]));
for (const m of matches) {
if (!names.has(m.home) || !names.has(m.away)) continue;
const d = m.homeGoals - m.awayGoals;
mini[m.home].gd += d;
mini[m.away].gd -= d;
if (d > 0) mini[m.home].pts += 3;
else if (d < 0) mini[m.away].pts += 3;
else { mini[m.home].pts += 1; mini[m.away].pts += 1; }
}
return [...group].sort((a, b) =>
mini[b.name].pts - mini[a.name].pts ||
mini[b.name].gd - mini[a.name].gd ||
b.gd - a.gd ||
b.gf - a.gf
);
}
Two things I got wrong at first. I compared the whole season's results instead of only the games between the tied teams, which gave the wrong order for three-way ties. And I forgot that a three-way tie can still leave two teams level after the mini table; in a strict reading you would run the rule again on that smaller group, which I have not added yet.
For a league of twelve teams this runs instantly, so I did not bother with anything clever. If you have rules that differ from mine, only the last return statement needs to change.
Top comments (0)