Carpool Rota: Fair Drivers, Swaps and Two Daily Runs
EasyAI-assisted coding · AI-Assisted Debugging
Carpool Rota: Fair Drivers, Swaps and Two Daily Runs
You've been asked to help the parents' committee at a primary school whose volunteers wrote a small carpool rota. Each school day the rota picks which families drive so that every child who is riding has a seat, and it tries to be fair about whose turn it is. Parents have started to complain about the rota's choices, and two features are queued behind the fixes. A "school day" is a Monday-to-Friday date written YYYY-MM-DD; holidays are not modelled.
Codebase
| File | What it does |
|---|---|
carpool/api.py | run(operations) entry point and the Dispatcher that routes each operation |
carpool/rota.py | Rota: registers families, marks unavailability and absences, plans days, answers roster questions |
carpool/planner.py | Counts riders, ranks candidate drivers, picks drivers until the seats cover the kids |
carpool/fairness.py | DriveCounts: drives each family has done so far |
carpool/availability.py | Who cannot drive on a day, and how many kids are away |
carpool/families.py | Family and the FamilyBook registry, argument validation helper |
carpool/schooldays.py | Date parsing and the weekday rule |
carpool/errors.py | RotaError and every error code |
carpool/swaps.py | SwapDesk stub for Part 2 (routes exist and answer "not supported") |
tests/test_rota.py | Existing end-to-end tests (some of them currently fail) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh rota and returns exactly one result per operation. Each operation is [name, *args]. Errors are returned as "error: <code>" strings, never raised, and a failed operation changes nothing.
| Operation | Arguments | Returns | Errors (checked in this order) |
|---|---|---|---|
family | family_id, seats, kids | true | invalid arguments, duplicate family |
unavailable | family_id, date | true | unknown family, invalid date, not a school day |
absent | family_id, date, count | true | unknown family, invalid date, not a school day, invalid count |
plan | date | list of driver family ids | invalid date, not a school day, already planned, cannot cover |
roster | date | list of driver family ids | invalid date, not a school day, not planned |
drives | family_id | number of drives so far | unknown family |
- An unknown operation name returns
"error: unknown operation". A wrong number of arguments, an operation that is not a list, or a badfamilyvalue returns"error: invalid arguments".family_idmust be a non-empty string;seatsandkidsmust be integers>= 0(true/falseare not integers). seatsis how many children the family's car can carry, not counting the driver. A family withseats = 0has no car and never drives.kidsis how many children it has.invalid datemeans the text is not a realYYYY-MM-DDdate (including2025-02-30,2025-3-3, extra spaces, or a non-string).not a school daymeans a real date on a Saturday or Sunday.absentsets how many of a family's kids are away on that date; a laterabsentfor the same family and date replaces the earlier number.countmust be an integer from0to the family'skids, otherwiseinvalid count.unavailablemarks a family as unable to drive on that date. Repeating it is harmless; there is no way to undo it.- Days can be planned in any order. Families may still be added after some days are planned; they take part in days planned afterwards.
Part 1: Fix the driver selection (RIDE-198, RIDE-203, RIDE-207)
Goal: make plan pick drivers by the rules below. Three complaints came in:
- "Our 2-seat car matched the two kids riding exactly, yet a second family got drafted too."
- "The rota keeps favouring whoever signed up first, even when everyone has driven equally."
- "A day the rota said it couldn't cover still counted against us."
Rules for plan
- Riders on a day = the sum over all families of
kids - absentfor that day. - Candidates are families with
seats > 0that have not marked that day unavailable. - Candidates are ordered by fewest drives so far, ties broken by family id, ascending (plain string comparison:
"F10" < "F9", uppercase before lowercase). - Take candidates from the front of that order, one at a time, only while the seats chosen so far are fewer than the riders. The chosen families' seats then cover the riders (
seats >= ridersis enough). The result lists the chosen ids in the order they were taken. - A day with zero riders needs no driver: the result is
[]and nobody's count changes, but the day is planned. - If all candidates together still cannot seat the riders, return
error: cannot cover: no counts change and the day is not marked as planned. Each chosen driver'sdrivescount rises by one only when the plan succeeds. Planning an already planned date returnsalready planned.
Worked example
[["family", "ng", 4, 2], ["family", "ortiz", 4, 3], ["family", "patel", 2, 2],
["unavailable", "ortiz", "2025-03-03"],
["absent", "patel", "2025-03-03", 1],
["plan", "2025-03-03"], ["plan", "2025-03-04"], ["drives", "ng"]]returns [true, true, true, true, true, ["ng", "patel"], ["ortiz", "ng"], 2]. On 03-03 six kids ride; ng (4 seats) and patel (2) seat exactly six. On 03-04 seven ride; ortiz has no drives yet, and ng beats patel on id.
tests/test_rota.py has tests for these complaints; some currently fail.
Part 2: Swaps and cover requests (RIDE-212)
Goal: after the rota is published, families ask to trade drives or hand a drive to a neighbour. Implement swap and cover in carpool/swaps.py (the dispatcher already routes to them).
| Operation | Arguments | Returns |
|---|---|---|
swap | date_a, family_a, date_b, family_b | true: family_a stops driving date_a and family_b stops driving date_b; each takes the other's day |
cover | date, from_family, to_family | true: to_family takes over from_family's drive on date |
Rules (checks run in this order; the first that fails is the error, and a failure changes nothing)
unknown family(forswap:family_afirst, thenfamily_b; forcover:from_family, thento_family), then the usual date errors (invalid date,not a school day, forswapdate_afirst).same familyif both families are the same; forswap,same slotif both dates are the same date.not plannedif either day has no plan (date_afirst).not drivingif the giving family is not on that day's roster (forswap:family_aondate_a, andfamily_bondate_b).already drivingif the receiving family is already on the roster it would join.unavailableif the receiving family cannot drive that day: it marked the day unavailable or has no car (seats = 0).cannot coverif, after the change, a day's drivers have fewer seats than the riders. The riders of a day are fixed at the moment it was planned; laterabsentcalls do not change them.- On success the new driver takes the old driver's position in the roster, and fairness counts follow: the family that gave up a drive loses one count, the one that took it gains one. (A
swaptherefore nets to zero until Part 3.)
The result of every earlier operation is a snapshot: a later swap or cover never changes a list that plan or roster already returned.
Worked example
[["family", "a", 3, 2], ["family", "b", 3, 1], ["family", "c", 2, 1],
["plan", "2025-03-03"], ["plan", "2025-03-04"],
["swap", "2025-03-03", "b", "2025-03-04", "c"],
["cover", "2025-03-04", "a", "c"],
["cover", "2025-03-04", "b", "c"],
["roster", "2025-03-03"], ["roster", "2025-03-04"], ["drives", "a"], ["drives", "c"]]returns [true, true, true, ["a","b"], ["c","a"], true, true, "error: already driving", ["a","c"], ["b","c"], 1, 2]. Four kids ride each day. After the swap the rosters are ["a","c"] and ["b","a"]; the cover then replaces a with c on 03-04, giving ["b","c"]. The last cover fails because c already drives that day.
Part 3: Morning and afternoon runs (RIDE-231)
Goal: the school now runs a morning run (am) and an afternoon run (pm) with their own rosters. Parents are available for one but not the other, and a family that did all the mornings should still be in line for afternoons. Every earlier rule keeps working for a single run; the differences are:
- A slot is a date plus a run:
"2025-03-03:pm". A bare date such as"2025-03-03"means"2025-03-03:am"forplan,roster,swapandcover. An unknown run (including"PM", an empty run,"2025-03-03:") giveserror: invalid run, checked after the date itself. - Each slot has its own plan, rider count and roster.
planon"d"and on"d:pm"are different days' work and either can come first;already plannedis per slot. - For
unavailableandabsenta bare date applies to both runs of that day, while"d:am"or"d:pm"applies only to that run. A bare-dateabsentreplaces both runs' figures; a later run-specific call replaces just that run. - Fairness is per run type. Rule 3 of Part 1 now compares drives done in the run being planned (an
amplan compares morning drives; ties still go to the smaller id). A family'spmdrives do not count for anamplan, and vice versa. drives family_idstill returns the family's total over both runs;drives family_id run("am"or"pm") returns just that run (invalid runfor anything else, checked afterunknown family).swapandcovertake slots. Availability and the no-car rule apply to the slot the receiver moves into. Counts move per run: in aswapbetween anamand apmslot,family_aloses anamdrive and gains apmdrive,family_bthe opposite. Forswapthedateerrors become slot errors (invalid date,not a school day,invalid run; slotafirst), andsame slotnow compares the full slots ("d"and"d:am"are the same slot).
Worked example
[["family", "a", 3, 2], ["family", "b", 3, 1], ["family", "c", 2, 1], ["family", "d", 3, 0],
["plan", "2025-03-03"], ["plan", "2025-03-03:pm"], ["plan", "2025-03-04:pm"],
["swap", "2025-03-03", "a", "2025-03-04:pm", "c"],
["plan", "2025-03-04"],
["drives", "a"], ["drives", "a", "am"], ["drives", "a", "pm"], ["drives", "c"]]returns [true, true, true, true, ["a","b"], ["a","b"], ["c","d"], true, ["a","d"], 3, 1, 2, 1]. Both 03-03 runs go to a and b (no one has driven either run yet). On 03-04:pm a and b have a pm drive each, so c and d drive. The swap makes the 03-03 morning ["c","b"] and the 03-04 afternoon ["a","d"]; a's counts become am 0, pm 2, c's am 1, pm 0. Planning 03-04 (morning) then looks at morning counts only (a 0, d 0, b 1, c 1) and picks a, then d.
Example 1
- Input:
- [["family","ng",4,2],["family","ortiz",4,3],["family","patel",2,2],["unavailable","ortiz","2025-03-03"],["absent","patel","2025-03-03",1],["plan","2025-03-03"],["plan","2025-03-04"],["drives","ng"]]
- Output:
- [true,true,true,true,true,["ng","patel"],["ortiz","ng"],2]
- Explanation
- On 03-03 six kids ride and ortiz cannot drive; ng (4) and patel (2) seat exactly six. On 03-04 seven ride; ortiz has the fewest drives, then ng beats patel on id.
Example 2
- Input:
- [["family","a",3,2],["family","b",3,1],["family","c",2,1],["plan","2025-03-03"],["plan","2025-03-04"],["swap","2025-03-03","b","2025-03-04","c"],["cover","2025-03-04","a","c"],["roster","2025-03-04"]]
- Output:
- [true,true,true,["a","b"],["c","a"],true,true,["b","c"]]
- Explanation
- b and c trade days, then c takes over a's 03-04 drive. Earlier plan results are not changed by later edits.
Guidelines
- Python 3 standard library only
- Dates are YYYY-MM-DD strings and only Monday-Friday are school days
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- A failed operation changes nothing (no counts, no rosters, no marks)
- Inputs are small; there is no performance requirement