As I am a hobbyist developer who is still exploring the depths of JavaScript, and who had never written or published a package before, I am certainly in no position to teach anything to anyone. What I can do, though, is to share my experience in writing what is essentially still , where the final table ended up looking like this
| Position | Team | Won | Drawn | Lost | GF | GA | GD | Points |
|---|---|---|---|---|---|---|---|---|
| 1 | Italy | 2 | 0 | 1 | 3 | 1 | +2 | 6 |
| 2 | Belgium | 2 | 0 | 1 | 4 | 2 | +2 | 6 |
| 3 | Republic of Ireland | 1 | 1 | 1 | 2 | 4 | -2 | 4 |
| 4 | Sweden | 0 | 1 | 2 | 1 | 3 | -2 | 1 |
GF = Goals For (goals scored), GA = Goals Against (goals conceded), GD = Goal Difference.
Italy and Belgium are tied on points, and the first tiebreaker at the Euros is goal difference (where Italy and Belgium are still tied, with +2 each), followed by the number of goals scored—at which point one would expect Belgium to triumph over Italy, with 4 goals scored against 3.
However, the Euros are a competition where a head-to-head style is used to sort teams that are even on points. This means that, as soon as we recognize that Italy and Belgium need to have their tie broken, we immediately calculate a new sub-table made only of the teams concerned in the tie. And as they played a single match that ended 0-2 for Italy in the very first matchday, this sub-table looks like this (soccer assigns three points for a win, one for a draw and none for a loss)
| Position | Team | Won | Drawn | Lost | GF | GA | GD | Points |
|---|---|---|---|---|---|---|---|---|
| 1 | Italy | 1 | 0 | 0 | 2 | 0 | +2 | 3 |
| 2 | Belgium | 0 | 0 | 1 | 0 | 2 | -2 | 0 |
GF = Goals For (goals scored), GA = Goals Against (goals conceded), GD = Goal Difference.
meaning that Italy is sorted above Belgium immediately, on account of head-to-head points (three to zero).
Different competitions will usually employ different styles (the Euros use the head-to-head style as we just saw, while for example the FIFA World Cup is famous for employing overall checks instead), though they will both switch to the other style should the first run of criteria be inconclusive in breaking a given tie.
This means that, potentially, head-to-head checks are bound to happen sooner or later (either immediately in a competition like the Euros, or as a potential second tiebreaking run at the FIFA World Cup). So my takeaway here was that the table we look at may potentially change during the sorting process, as the head-to-head style (whether it comes immediately or not) hinges on this fact.
But this was not my only takeaway, as the following example shows.
More intricacies: head-to-head reapplication
Belgium is once again the protagonist in the famous -d.), a recomputation of the head-to-head results does not happen at every step, but only once the full list of tiebreakers has run out. Since after the tie in points and goal difference there is still the number of goals scored to be checked, we look at that and this is why Romania ends up triumphing. Had that been tied as well, only at that point would we actually start considering a sub-table based on the single match played between the still-tied teams (Belgium and Romania), where Belgium would in fact come out on top due to their win.
So here is the second takeaway: there is a concept of depth involved in the sorting process, as depending on how far you are into it you have to take different decisions—such as whether to proceed with a sub-table recalculation or not. In this case, you would not proceed with it just yet as the list of criteria is still going on.
These are the main points that lead to my decision for the form of my sorting function.
A recursive approach
The sorting algorithm, accessing via the .standings() method of the class that my package implements, relies on a recursive function
const sortAndDivideTable = (table, iteration, criteria) => {
// ...
}
where at any given step table is the table holding the data of the teams that are currently to be sorted, iteration keeps track of the iteration number and other related information (e.g. if this is a head-to-head or overall type of check), and criteria is an array representing the ordered list of criteria to be applied (e.g. points, goal difference, number of goals scored).
The recursion starts with a table that is computed from all the matches played by all teams, and which is still potentially unordered. Notice that the first iteration of the algorithm is always of type overall (and is therefore initialized as such when the recursion starts), as by definition the first check is always the number of points obtained across all matches; again by definition, the first element in the criteria array is always "points".
This starting table is also safely kept tucked away: at any given step its entries are not modified, but its rows will be sorted little by little. For reference purposes, we can call it the ‘original standings’ from now on.
A real-world example from the UEFA Champions League
Having this in mind, it is possible to make sense of the algorithm with an example that is provided by / saw all teams finish their group with eight points each: how were these ties resolved? and who can say what happens at the Euros should two teams be equal on points, goal difference and goals…
SOCIAL SHARE CARD GENERATOR