Skip to content

fix(hy3): route H1/H2 DQ detail to the matching slot for a prelim+finals double-DQ - #30

Open
fsalum wants to merge 2 commits into
SwimComm:masterfrom
fsalum:fix/h1-double-dq-slot-match
Open

fix(hy3): route H1/H2 DQ detail to the matching slot for a prelim+finals double-DQ#30
fsalum wants to merge 2 commits into
SwimComm:masterfrom
fsalum:fix/h1-double-dq-slot-match

Conversation

@fsalum

@fsalum fsalum commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Stacked on #29 ("type Event.distance as float for non-integer distances").
This branch is based on #29's branch, so #29 needs to merge first. Until it
does, this PR shows #29's commit alongside its own; after #29 lands it collapses
to the single DQ-parser commit (h_dq_parsers.py + its test).

Problem

h1_parser raises AssertionError("DQ Codes should match in the H1 line") on
any swim disqualified in both prelims and finals of the same event for
different infractions — a common case in IM events and championship
prelims/finals formats.

Such a swim's prelim and finals arrive as separate E1/E2 records that resolve
to one shared EventEntry carrying both prelim_dq_info and finals_dq_info.
h1_parser resolves the H1 detail line to a slot by fixed
finals -> swimoff -> prelim priority, takes the first populated slot, and
then asserts that slot's stored DQ code equals the H1's own code. Records arrive
finals-first, so by the time the prelim's H1 is parsed the finals slot is
already populated with a different code:

E2F …Q7T   -> finals slot code = 7T
H17T…      -> finals slot set, 7T == 7T ✓  (attaches to finals)
E2P …Q1M   -> prelim slot code = 1M
H11M…      -> resolves finals-first, takes finals (7T); 7T != 1M -> AssertionError

The assertion aborts the H1 line and the prelim DQ's human-readable reason
string is lost.

Two related latent defects sit behind the visible assertion:

  1. When the two slots happen to share a DQ code the assertion does not fire,
    but finals-first resolution still attaches the reason to the wrong slot (both
    H1s land on finals).
  2. h2_parser uses the identical finals-first resolution with no assertion at
    all, so H2 detail text is silently mis-attributed in the same double-DQ case.

Fix

h1_parser now resolves to the slot whose stored DQ code matches the H1's
own code, preferring a slot not yet filled so the two H1s of a same-code
double-DQ fill their own slots in file order. When no populated, unfilled slot's
code matches, it degrades to a no-op — the same graceful behavior the existing
no-slot guard already used (the DQ status and code are recorded by the E2/F2
result line regardless, so only the reason string is ever at stake). The
assert is removed.

h2_parser cannot match on its own code — an H2's 2-char code is the specific
stroke/leg infraction (e.g. 2L), not the slot's DisqualificationCode (e.g.
the relay-leg code 6A). So h1_parser records the slot it resolved (in the
opts dict threaded through every line parser) and h2_parser attaches its
detail to that same slot, falling back to the first populated slot when no H1
preceded it.

Tests

Added to tests/hy3/line_parsers/test_h_dq_parsers.py:

  • prelim/finals H1s of a different-code double-DQ each route to their own slot
    (previously raised);
  • same-code double-DQ fills each slot in file order rather than finals twice;
  • an H1 whose code matches no populated slot is a no-op, not a raise;
  • a relay leg code (6A) still attaches;
  • an H2's detail follows its immediately-preceding H1's slot across a double-DQ.

All existing DQ-parser tests continue to pass.

fsalum and others added 2 commits August 16, 2026 16:14
The E1/F1 distance field (6 chars) can contain non-integer values — e.g.
"2.4" for 2.4 miles in open-water meets. safe_cast(int, "2.4") raised
ValueError and fell back to int() = 0, permanently losing the value.

Type Event.distance as float and parse it with safe_cast(float, ...) so
these distances survive. Integer distances now come through as floats
(100 -> 100.0); this is an API-compatible data change, so callers reading
distance as an int should coerce. Blank/non-numeric fields still yield 0.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A swim disqualified in both prelims and finals of the same event resolves
to a single entry carrying both prelim_dq_info and finals_dq_info. h1_parser
resolved the detail line to a slot by fixed finals -> swimoff -> prelim
priority and asserted the chosen slot's stored code equalled the H1's code.
Records arrive finals-first, so a prelim H1 was checked against the already
populated finals slot: when the two DQ infractions differ (common in IM and
championship formats) the codes mismatch and h1_parser raised
AssertionError("DQ Codes should match in the H1 line"), aborting the reason
string. When the two slots happened to share a code no assertion fired but
the reason was still attached to the wrong slot.

Resolve instead to the slot whose stored DQ code matches the H1's own code,
preferring an unfilled slot so the two H1s of a same-code double-DQ fill
their own slots in file order. No-op when no populated, unfilled slot's code
matches — the same graceful degrade the no-slot case already used; the DQ
status and code are recorded by the E2/F2 result line regardless.

h2_parser used the identical finals-first resolution with no assertion, so
H2 detail text was silently mis-attributed in the same double-DQ case. H2
cannot match on its own code (it is the stroke/leg infraction, not the
slot's DisqualificationCode), so h1_parser now records the slot it resolved
and h2_parser attaches its detail to that same slot, falling back to the
first populated slot when no H1 preceded it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant