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
7 changes: 7 additions & 0 deletions hytek_parser/hy3/line_parsers/e_event_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def e1_parser(
# The two are mutually exclusive; col 77 takes precedence.
meet_division = extract(line, 77, 3) or extract(line, 92, 2) or None
exhibition = extract(line, 84, 1) == "X"
entry_flag = extract(line, 96, 1) or None

entry = event.get_or_create_entry(
swimmers=entry_swimmers,
Expand All @@ -79,6 +80,12 @@ def e1_parser(
converted_seed_time_course=entry_converted_seed_time_course,
meet_division=meet_division,
exhibition=exhibition,
# The entry's own event fields (see EventEntry): the Event is keyed by
# number alone and keeps only the first distance seen for it.
distance=distance,
stroke=stroke,
course=event_course,
entry_flag=entry_flag,
)

# Update event
Expand Down
5 changes: 5 additions & 0 deletions hytek_parser/hy3/line_parsers/f_relay_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ def f1_parser(
converted_seed_time_course=entry_converted_seed_time_course,
relay_team_id=relay_team,
relay_swim_team_code=team_code,
# The entry's own event fields (see EventEntry).
distance=distance,
stroke=stroke,
course=event_course,
entry_flag=extract(line, 96, 1) or None,
)

# Update event
Expand Down
34 changes: 21 additions & 13 deletions hytek_parser/hy3/line_parsers/g_split_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,34 @@ def g1_parser(
event_num, event = file.meet.last_event
entry = event.last_entry

# Read splits
line_pos = 4
splits: dict[int, float] = {}
while line_pos < 124 and line[line_pos] != " ":
split_num = safe_cast(int, extract(line, line_pos, 2))
split_time = safe_cast(float, extract(line, line_pos + 2, 8))

splits[split_num] = split_time
line_pos += 11 # MM for some reason specifies P/S/F every time???

# Set correct attribute
if event_type == ResultType.PRELIM:
entry.prelim_splits |= splits
target = entry.prelim_splits
elif event_type == ResultType.SWIMOFF:
entry.swimoff_splits |= splits
target = entry.swimoff_splits
elif event_type == ResultType.FINAL:
entry.finals_splits |= splits
target = entry.finals_splits
else:
raise ValueError("Invalid event type!")

# Read splits. The index is a two-character field, so it wraps at 100:
# a swim with more than 49 split slots (a 1500 LCM or 1650 SCY recorded
# every 25) continues "...F96 ... F00 ... F04 ..." on the next G1 line,
# and read literally the wrapped indexes collide with the early ones and
# overwrite them. Indexes are monotonic within an entry, so a value below
# the highest one already seen for this slot means the field wrapped.
line_pos = 4
highest = max(target) if target else -1
while line_pos < 124 and line[line_pos] != " ":
split_num = safe_cast(int, extract(line, line_pos, 2))
split_time = safe_cast(float, extract(line, line_pos + 2, 8))

while split_num <= highest and highest >= 0:
split_num += 100
highest = max(highest, split_num)
target[split_num] = split_time
line_pos += 11 # MM for some reason specifies P/S/F every time???

event.last_entry = entry
file.meet.last_event = (event_num, event)

Expand Down
38 changes: 37 additions & 1 deletion hytek_parser/hy3/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,22 @@ class EventEntry:
meet_division: Optional[str] = None
exhibition: bool = False

# The entry's OWN event fields, as written on its E1/F1 line (cols 16-22
# and 51). An Event is keyed by event number alone, so when one number
# carries more than one distance -- a combined-distance event where
# swimmers choose 400 or 500, 1000 or 1650, or a split-request entry that
# records an intermediate split as its own official time -- the Event's
# distance is only the first one seen. These fields say what THIS entry
# swam. None on entries built before the parser set them.
distance: Optional[float] = None
stroke: Optional[Stroke] = None
course: Optional[Course] = None
# Col 96 of E1/F1. Observed values: "S" and "T" on split-request entries
# (an intermediate split recorded as an official time for a shorter
# distance; fee and seed are 0 and the distance differs from the event's).
# Blank on ordinary entries. Semantics of "S" vs "T" are not documented.
entry_flag: Optional[str] = None

def __init__(
self,
swimmers: dict[int, Swimmer],
Expand All @@ -193,6 +209,10 @@ def __init__(
relay_swim_team_code: Optional[str] = None,
meet_division: Optional[str] = None,
exhibition: bool = False,
distance: Optional[float] = None,
stroke: Optional[Stroke] = None,
course: Optional[Course] = None,
entry_flag: Optional[str] = None,
) -> None:
self.swimmers = swimmers
self.relay = relay
Expand All @@ -205,6 +225,10 @@ def __init__(
self.relay_swim_team_code = relay_swim_team_code
self.meet_division = meet_division
self.exhibition = exhibition
self.distance = distance
self.stroke = stroke
self.course = course
self.entry_flag = entry_flag

for course in ("prelim", "swimoff", "finals"):
setattr(self, f"{course}_time", None)
Expand Down Expand Up @@ -237,7 +261,8 @@ def same_swimmer_entry_as(self, other: "EventEntry") -> bool:
relay_swim_team_code, seed_time, seed_course) — not swimmers, which
are populated later by F3.
For individuals the identity is the existing (swimmers, event_number,
seed_time, seed_course, converted_seed_time, converted_seed_time_course).
seed_time, seed_course, converted_seed_time, converted_seed_time_course)
plus the entry's own distance.
"""
if self.relay or other.relay:
return (
Expand All @@ -250,6 +275,9 @@ def same_swimmer_entry_as(self, other: "EventEntry") -> bool:
)
return (
self.swimmers == other.swimmers
# An entry at a different distance is a different swim, even for
# the same swimmer in the same event (a split-request entry).
and self.distance == other.distance
and self.event_number == other.event_number
and self.seed_time == other.seed_time
and self.seed_course == other.seed_course
Expand Down Expand Up @@ -296,6 +324,10 @@ def get_or_create_entry(
relay_swim_team_code: Optional[str] = None,
meet_division: Optional[str] = None,
exhibition: bool = False,
distance: Optional[float] = None,
stroke: Optional[Stroke] = None,
course: Optional[Course] = None,
entry_flag: Optional[str] = None,
) -> EventEntry:
"""Get an event entry or create one if needed."""
entry = EventEntry(
Expand All @@ -310,6 +342,10 @@ def get_or_create_entry(
relay_swim_team_code=relay_swim_team_code,
meet_division=meet_division,
exhibition=exhibition,
distance=distance,
stroke=stroke,
course=course,
entry_flag=entry_flag,
)
if self.entries and self.entries[-1].same_swimmer_entry_as(entry):
# P/F entries always listed together: a swimmer (individuals) or a
Expand Down
114 changes: 114 additions & 0 deletions tests/hy3/line_parsers/test_e_event_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from hytek_parser.hy3.line_parsers.d_swimmer_parsers import d1_parser
from hytek_parser.hy3.line_parsers.e_event_parsers import e1_parser, e2_parser
from hytek_parser.hy3.schemas import Meet, Team, Gender, Stroke
from hytek_parser.hy3.enums import Course

class TestEEventParser(unittest.TestCase):

Expand Down Expand Up @@ -369,3 +370,116 @@ def test_non_dq_clears_anchor(self):

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


class TestEntryOwnEventFields(unittest.TestCase):
"""An entry keeps the distance/stroke/course written on its own E1 line.

Events are keyed by event number, so when one number carries more than
one distance the Event holds only the first distance seen. The entry's
own fields are what that entry actually swam.
"""

def _file(self):
opts = {"default_country": "USA"}
file = ParsedHytekFile()
file.meet = Meet()
file.meet.last_team = ("FOO", Team("Foo Bar", "FOO", "foo","","","","","","","","","","","",{}))
d_line = "D1F 27Hansen Mads 10272010 13 27"
return d1_parser(d_line, file, opts), opts

def test_entry_carries_its_own_fields(self):
file, opts = self._file()
e_line = "E1F 27HanseFG 1000A 13 14 0S 4.25 7A 715.47Y 715.47Y 3.00 0.00 NN N "
file = e1_parser(e_line, file, opts)
entry = file.meet.events["7A"].last_entry
self.assertEqual(1000.0, entry.distance)
self.assertEqual(Stroke.FREESTYLE, entry.stroke)
self.assertEqual(Course.SCY, entry.course)
self.assertIsNone(entry.entry_flag)

def test_split_request_entry_is_a_second_entry_at_its_own_distance(self):
"""A meet-management export can record an intermediate split as its
own official time: a second E1 for the same event number with the
shorter distance, fee 0, seed 0 and an "S" flag in column 96, followed
by an E2 carrying the split time. It must land as its own entry at
its own distance, while the Event keeps the distance it was created
with."""
file, opts = self._file()
main = "E1F 27HanseFG 1000A 13 14 0S 4.25 7A 715.47Y 715.47Y 3.00 0.00 NN N "
e2_main = "E2F 707.50Y 0 4 7 6 14 0 707.56 707.49 707.52 707.50 0.00 12032009 "
split = "E1F 27HanseFG 500A 13 18 0A 0.00 7A 0.00 0.00 0.00 0.00 NN SN "
e2_split = "E2F 351.55Y 0 1 6 3 2 0 0.00 0.00 0.00 0.00 0.00 "
for ln, fn in ((main, e1_parser), (e2_main, e2_parser), (split, e1_parser), (e2_split, e2_parser)):
file = fn(ln, file, opts)
event = file.meet.events["7A"]
self.assertEqual(1000.0, event.distance)
self.assertEqual(2, len(event.entries))
first, second = event.entries
self.assertEqual((1000.0, None, 707.50), (first.distance, first.entry_flag, first.finals_time))
self.assertEqual((500.0, "S", 351.55), (second.distance, second.entry_flag, second.finals_time))

def test_prelim_and_finals_of_one_swim_still_merge(self):
"""The distance joins the entry identity; a prelim + finals re-listing
at the same distance still folds into one entry as before."""
file, opts = self._file()
p = "E1F 27HanseFG 100A 13 14 0S 4.25 71 61.05Y 61.05Y 1.00 0.00 NN N "
e2p = "E2P 59.85Y 0 3 1 6 16 0 59.83 60.01 59.65 59.85 0.00 12052009 "
e2f = "E2F 59.26Y 0 1 8 8 16 0 59.34 59.25 59.25 59.26 0.00 12052009 "
file = e1_parser(p, file, opts); file = e2_parser(e2p, file, opts)
file = e1_parser(p, file, opts); file = e2_parser(e2f, file, opts)
event = file.meet.events["71"]
self.assertEqual(1, len(event.entries))
self.assertEqual((59.85, 59.26), (event.entries[0].prelim_time, event.entries[0].finals_time))

def test_combined_distance_event_keeps_each_entrys_distance(self):
"""One event number, two distances (swimmers choose 400 or 500)."""
file, opts = self._file()
d2 = "D1F 28Smith Jane 10272010 13 28"
file = d1_parser(d2, file, opts)
a = "E1F 27HanseFG 500A 13 14 0S 4.25 41C 356.02Y 356.02Y 1.00 0.00 NN N "
b = "E1F 28SmithFG 400A 13 14 0S 4.25 41C 290.00L 290.00L 1.00 0.00 NN N "
file = e1_parser(a, file, opts); file = e1_parser(b, file, opts)
event = file.meet.events["41C"]
self.assertEqual(500.0, event.distance)
self.assertEqual([500.0, 400.0], [e.distance for e in event.entries])


class TestG1SplitIndexWrap(unittest.TestCase):
"""The G1 split index is a two-character field and wraps at 100."""

def test_wrapped_indexes_continue_instead_of_overwriting(self):
from hytek_parser.hy3.line_parsers.g_split_parsers import g1_parser
opts = {"default_country": "USA"}
file = ParsedHytekFile()
file.meet = Meet()
file.meet.last_team = ("FOO", Team("Foo Bar", "FOO", "foo","","","","","","","","","","","",{}))
d = "D1M 27Hansen Mads 10272010 13 27"
e1 = "E1M 27HanseMB 1500A 15109 0A 10.00 12C 0.00L 0.00L 0.00 0.00 NN N 10"
e2 = "E2F 1087.01L 0 1 5 2 2 0 0.00 0.00 0.00 1087.01 0.00 05292021K 0 46"
g1a = "G1F 4 0.00F 8 66.75F12 0.00F16 139.54F20 0.00F24 212.15F28 0.00F32 284.59F36 0.00F40 357.38F44 0.00 42"
g1b = "G1F48 431.18F52 0.00F56 504.48F60 0.00F64 577.28F68 0.00F72 651.37F76 0.00F80 725.04F84 0.00F88 798.37 63"
g1c = "G1F92 0.00F96 871.63F00 0.00F04 945.32F08 0.00F12 1018.09F16 0.00F20 1087.01 79"
file = d1_parser(d, file, opts); file = e1_parser(e1, file, opts); file = e2_parser(e2, file, opts)
for ln in (g1a, g1b, g1c):
file = g1_parser(ln, file, opts)
splits = file.meet.events["12C"].last_entry.finals_splits
self.assertEqual(30, len(splits))
self.assertEqual(66.75, splits[8]) # early split survives
self.assertEqual(945.32, splits[104]) # "04" after the wrap -> 104
self.assertEqual(1087.01, splits[120]) # the finish, "20" -> 120
self.assertEqual(list(range(4, 121, 4)), sorted(splits))

def test_unwrapped_swim_is_unchanged(self):
from hytek_parser.hy3.line_parsers.g_split_parsers import g1_parser
opts = {"default_country": "USA"}
file = ParsedHytekFile()
file.meet = Meet()
file.meet.last_team = ("FOO", Team("Foo Bar", "FOO", "foo","","","","","","","","","","","",{}))
d = "D1F 27Hansen Mads 10272010 13 27"
e1 = "E1F 27HanseFG 100A 13 14 0S 4.25 71 61.05Y 61.05Y 1.00 0.00 NN N "
e2 = "E2F 59.26Y 0 1 8 8 16 0 59.34 59.25 59.25 59.26 0.00 12052009 "
g1 = "G1F 2 28.47F 4 59.26 "
file = d1_parser(d, file, opts); file = e1_parser(e1, file, opts); file = e2_parser(e2, file, opts)
file = g1_parser(g1, file, opts)
self.assertEqual({2: 28.47, 4: 59.26}, file.meet.events["71"].last_entry.finals_splits)