Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions hytek_parser/hy3/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ class Stroke(Enum):
BREASTSTROKE = "C", "3", 3
BUTTERFLY = "D", "4", 4
MEDLEY = "E", "5", 5
# Diving. Hy-Tek encodes the BOARD in the stroke column:
# F = 1-metre springboard, G = 3-metre springboard, H = platform.
# Verified on a multi-conference corpus: the chars nest strictly
# (F, then F+G, then F+G+H -- never G without F, never H without G),
# carry the same dive count within a meet, and their median scores
# ascend with degree of difficulty.
# Without these members all three fall through select_from_enum() to
# UNKNOWN, which is the catch-all for any unrecognized byte -- making
# diving indistinguishable from file corruption.
DIVING_1M = "F"
DIVING_3M = "G"
DIVING_PLATFORM = "H"

UNKNOWN = "U", "0", 0

Expand Down
9 changes: 8 additions & 1 deletion hytek_parser/hy3/line_parsers/f_relay_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,14 @@ def f3_parser(
# Out of swimmers
break

swimmer = file.meet.swimmers[swimmer_meet_id]
swimmer = file.meet.swimmers.get(swimmer_meet_id)
if swimmer is None:
# The F3 leg references a swimmer meet-id with no D1 roster record in
# this file — seen in some incomplete meet exports (e.g. relay legs for
# athletes the roster section omits). Skip the leg rather than raising a
# KeyError; the relay keeps its other legs. Mirrors the tolerance the
# empty-swimmers and absent-leg-1 cases already get below.
continue
swimmer_leg = safe_cast(int, extract(line, 15 + offset, 1))

# Hy-Tek encodes legs 1..8; preserve the leg number as-is.
Expand Down
10 changes: 7 additions & 3 deletions hytek_parser/hy3/line_parsers/h_dq_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ def h1_parser(
dq_code = select_from_enum(DisqualificationCode, extract(line, 3, 2))
dq_info = extract(line, 5, 124) # Whitespace is stripped

assert (
entry.prelim_dq_info or entry.swimoff_dq_info or entry.finals_dq_info
), "There must be a DQ for there to be an H1 line"
# No-op if the entry carries no DQ slot to attach to — mirrors h2_parser.
# An H1 can appear whose last_entry is not the DQ'd swim it describes (e.g. a
# relay DQ, or a non-DQ entry emitted between the DQ result and its H1); skip
# the detail rather than raising. The DQ result itself is unaffected, only the
# human-readable reason string is dropped.
if not (entry.prelim_dq_info or entry.swimoff_dq_info or entry.finals_dq_info):
return file

if entry.finals_dq_info:
# DQ happened in prelims
Expand Down
28 changes: 28 additions & 0 deletions tests/hy3/line_parsers/test_f_relay_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,34 @@ def test_f3_with_8_swimmers_keeps_all_8_keyed(self):
entry = result.meet.last_event[1].last_entry
self.assertEqual(set(entry.swimmers.keys()), {1, 2, 3, 4, 5, 6, 7, 8})

@staticmethod
def _f3_line(pairs):
# Build an F3 line from (meet_id, leg) pairs. Each 13-char block is a
# 5-char right-justified meet_id (cols 4-8), 6 pad, 1-char leg (col 15), pad.
line = "F3 "
for mid, leg in pairs:
line += f"{mid:>5d}" + " " * 6 + f"{leg:d}" + " "
return line

def test_f3_unrostered_swimmer_id_is_skipped_not_raised(self):
# Regression: an F3 relay leg referencing a swimmer meet-id with no D1
# roster record in the file must be skipped, not raise KeyError. Roster
# has ids 1..8; id 42 is absent, so leg 1 is dropped and 2..4 are kept.
file, opts = self._file_with_relay_entry_and_4_swimmers()
f3_line = self._f3_line([(42, 1), (2, 2), (3, 3), (4, 4)])
result = f3_parser(f3_line, file, opts) # must not raise
entry = result.meet.last_event[1].last_entry
self.assertEqual(set(entry.swimmers.keys()), {2, 3, 4})

def test_f3_all_unrostered_yields_empty_swimmers_no_raise(self):
# Every leg references an absent id → empty swimmers dict, still no raise
# (the empty-dict age guard already handles this downstream).
file, opts = self._file_with_relay_entry_and_4_swimmers()
f3_line = self._f3_line([(40, 1), (41, 2), (42, 3), (43, 4)])
result = f3_parser(f3_line, file, opts) # must not raise
entry = result.meet.last_event[1].last_entry
self.assertEqual(entry.swimmers, {})


class TestF2BackupTimingFields(unittest.TestCase):
"""F2 timing fields. Same offsets as E2 for the five timing
Expand Down
35 changes: 35 additions & 0 deletions tests/hy3/line_parsers/test_h_dq_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,40 @@ def test_h2_empty_detail_is_none(self) -> None:
self.assertIsNone(entry.finals_dq_info.info_str_detail)


class TestH1DqParser(unittest.TestCase):

def _file_with_entry(self):
opts = {"default_country": "USA"}
file = ParsedHytekFile()
file.meet = Meet()
file.meet.last_team = ("FOO", Team("Foo Bar", "FOO", "foo", "", "", "", "", "", "", "", "", "", "", "", {}))
d_line = "D1M 27Hansen Mads 10272010 13 27"
e_line = "E1M 27HanseXX 50D 11109 0U 0.00 22X 37.41S 37.41S 0.00 0.00 0NN N 70"
file = d1_parser(d_line, file, opts)
file = e1_parser(e_line, file, opts)
return file, opts

def test_h1_sets_reason_on_finals_dq(self) -> None:
from hytek_parser.hy3.line_parsers.h_dq_parsers import h1_parser
file, opts = self._file_with_entry()
entry = file.meet.last_event[1].last_entry
entry.finals_dq_info = DisqualificationInfo(DisqualificationCode.FLY_KICK_ALTERNATING, "")
# "H1" + "1A" (FLY_KICK_ALTERNATING, matches the slot's code) + reason text
result = h1_parser("H11AAlternating Kick", file, opts)
self.assertEqual("Alternating Kick", result.meet.last_event[1].last_entry.finals_dq_info.info_str)

def test_h1_no_dq_is_noop(self) -> None:
# Regression: an H1 line whose entry carries no DQ slot must be a no-op,
# not raise — mirroring h2_parser. (A relay DQ, or a non-DQ entry emitted
# between the DQ result and its H1, resolves last_entry to a non-DQ entry.)
from hytek_parser.hy3.line_parsers.h_dq_parsers import h1_parser
file, opts = self._file_with_entry()
result = h1_parser("H11AAlternating Kick", file, opts) # must not raise
entry = result.meet.last_event[1].last_entry
self.assertIsNone(entry.finals_dq_info)
self.assertIsNone(entry.swimoff_dq_info)
self.assertIsNone(entry.prelim_dq_info)


if __name__ == "__main__":
unittest.main()