After my last post about sorting a league table, my friend in Namangan sent me a real case from this season. Three teams finished on the same points, and the head-to-head mini table left two of them still level. My first version simply stopped there, so the order of those two came down to whatever the sort happened to do.
A strict reading of the rule turns out to be recursive: once the mini table separates one team from the rest, you take the teams that are still level and apply the same rule again, this time using only the games between them. To see how real tables look when teams share points, I keep the standings on the FC Navbahor site open in another tab.
The fix is small. Instead of sorting the group once by the mini table, I split the sorted group into sub-groups of equal mini-table values and call the same function on every sub-group that is still bigger than one team.
function breakTie(group, matches) {
if (group.length < 2) return group;
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; }
}
const sorted = [...group].sort((a, b) =>
mini[b.name].pts - mini[a.name].pts ||
mini[b.name].gd - mini[a.name].gd
);
const key = t => `${mini[t.name].pts}:${mini[t.name].gd}`;
const parts = [];
for (const t of sorted) {
const last = parts[parts.length - 1];
if (last && key(last[0]) === key(t)) last.push(t);
else parts.push([t]);
}
if (parts.length === 1) {
return sorted.sort((a, b) => b.gd - a.gd || b.gf - a.gf);
}
return parts.flatMap(p => breakTie(p, matches));
}
Two details matter here. The first is the stop condition: if the mini table does not split the group at all (everyone is still level on the mini points and difference), calling the function again would loop forever, so in that case I fall back to the overall goal difference and goals scored, exactly as before. The second is that every recursive call rebuilds the mini table from scratch for the smaller group, so a game against the team that has already been placed no longer counts.
I tested it on a made-up table of five teams where three of them are level, and on the case from my friend's league. Both give the order he expected. If your competition uses away goals or fair play points as a further step, add them to the final fallback and leave the recursion alone.
Top comments (0)