The problemWhere it started
An athletics federation runs tournaments where every event begins with a sorted start list: athletes grouped by age category per the rulebook, distributed across heats and lanes, separated by club so teammates don't run side-by-side. Producing that list used to mean an evening in Excel: pulling each athlete into the right band by birth year, balancing heats so the last one isn't half empty, double-checking every row because one slipped category invalidates the heat. The work was technically simple and mind-numbingly tedious, and it had to be redone from scratch for every meet.
ApproachWhat we did
- 1
Read the federation's existing Excel registration file directly. No new template, no fresh data entry, no parallel source of truth.
- 2
Encode the full age-category and group-sizing rulebook in versioned code so a rulebook change is one commit, not a season of re-learning.
- 3
Balance heats so empty lanes always fall in the slowest heat, never the fastest. The last heat is always full.
- 4
Separate clubs across lanes within each heat so teammates don't run side-by-side, and flag the edge cases where separation is mathematically impossible.
- 5
Ship as a single .bat file the client double-clicks. No Python install, no terminal, no internet, no IT.
The resultThe numbers after launch
DetailsHow it works
Read what they already have
Before each event the federation distributes a single Excel file with every registered athlete: name, club, birth year, registered event, personal best. That file is the source of truth and it isn't going anywhere. The first decision was to make the tool read it directly, exactly as it arrives, instead of asking the federation to fill out a cleaner template. New templates die on contact with reality. The existing file is what people are already typing into, the file they already trust to be right, and the file they'll keep using whether this tool exists or not.
So the parser absorbs their format with all its idiosyncrasies: empty rows where a club's section ends, manual notes in stray columns, occasional cells with two birth years separated by a slash, club names that drift between abbreviations and full versions row to row. Each idiosyncrasy is a small parsing rule at the edge. The point is that downstream every piece of logic gets to assume clean data, because the mess was already absorbed.
Encode the rulebook
Athletics has age categories defined by birth year, lane assignments per heat, and rules about how clubs can be split across the field. The rulebook is a public document, but applying it correctly to a list of two hundred athletes is what a person used to spend an evening doing.
Every category boundary is one line of Python. When the federation pushed the youngest seniors band down a year mid-season, eliminating the junior-senior category and re-shuffling everything above and below it, updating the tool was a single edit to the categories table. Heat composition, group sizing, the printed output, all recomputed correctly from that one change. That's the whole point of writing the spec as code: the spec moves and the code moves with it, in minutes instead of weeks.
Balance the heats
The seeding rule is simple to state and tedious to apply by hand. The fastest athletes go in the last heat, the slowest in the first. When the total isn't a clean multiple of the lane count, somebody runs in a half-empty heat. The federation's preference, which feels obvious once you hear it, is that the empty lanes always belong to the slowest heat. The fastest competitors deserve a full field; the slowest deserve room.
So if forty-seven athletes have to fit eight lanes, the heats come out as [7, 8, 8, 8, 8], not [8, 8, 8, 8, 7]. After heats are sized, athletes get assigned to specific lanes per the federation's lane-priority chart. Then a club-separation pass walks the assignment and swaps lanes within a heat whenever two teammates landed adjacent, falling back to a wider swap if no neighbour swap is possible. There's a narrow set of cases where club separation is mathematically impossible, when a single club fills more than half the field, and those cases are flagged in the output rather than silently broken.
Ship as a double-click
The client is one person who runs tournaments out of a folder of spreadsheets. They use Windows. They don't want a web app, an account, or a login. They want to drop an Excel file on a button and get an Excel file back, on a Sunday morning before a meet, without thinking about software.
The whole tool ships as a folder with a .bat file inside. Double-click runs the program, reads the input, writes the output to the same folder, and closes. No Python to install, the interpreter is bundled. No command line to learn. No internet connection required. The first version was built and handed over in three weeks; since then, every change request has been a small revision to the rulebook or the output formatting, shipped as an updated folder dropped in the same place. The tool fits the team's rhythm instead of asking the team to adapt to it.