Skip to content

Events & Time

Events

Event is a 90+ member enum; each member is a (distance, stroke, course) tuple, named <STROKE>_<DISTANCE>_<COURSE> for individual events (e.g. FREE_100_SCY) and <STROKE>_<DISTANCE>_RELAY_<COURSE> for relays (e.g. MEDLEY_400_RELAY_LCM). Members compare in declaration order, so sorted(...) yields a natural event ordering.

  • Components: event.distance, event.stroke (Stroke), event.course (Course).
  • Lookup: Event.find(distance, stroke, course) → the matching Event or None.
  • Relays: event.is_relay(), event.leg_distance(), event.leg_strokes(), and event.leg_event(order) — the individual event swum on a given leg (1–4). This is what lets a relay leg sort alongside flat-start swims of the same individual event.

Time

Time stores centiseconds: int and formats as [M:]SS.HH. See the data-model guide for usage; the full API is below.

event

Swimming event representation as (distance, stroke, course).

Event

Bases: Enum

A unique swimming event represented by a (distance, stroke, course) tuple.

Supports declaration-order comparison. Members are named with uppercase patterns using abbreviated stroke names, e.g. <STROKE>_<DISTANCE>_<COURSE> (for individual events like FREE_50_SCY) or <STROKE>_<DISTANCE>_RELAY_<COURSE> (for relays like MEDLEY_200_RELAY_LCM).

distance property
distance: int

Total event distance in yards/meters.

stroke property
stroke: Stroke

The event stroke.

course property
course: Course

The event course (SCY, SCM, or LCM).

find classmethod
find(
    distance: int, stroke: Stroke, course: Course
) -> Event | None

Resolve an event by components, or None if none matches.

Source code in src/tunas/event.py
@classmethod
def find(cls, distance: int, stroke: Stroke, course: Course) -> Event | None:
    """Resolve an event by components, or None if none matches."""
    return _BY_COMPONENTS.get((distance, stroke, course))
is_relay
is_relay() -> bool

True if this is a relay event (freestyle or medley relay).

Source code in src/tunas/event.py
def is_relay(self) -> bool:
    """True if this is a relay event (freestyle or medley relay)."""
    return self.stroke in (_FREE_R, _MEDLEY_R)
leg_distance
leg_distance() -> int

Calculate the per-leg distance (total distance divided by 4).

Returns:

Type Description
int

The distance of a single leg in the relay.

Raises:

Type Description
ValueError

If the event is an individual swim rather than a relay.

Source code in src/tunas/event.py
def leg_distance(self) -> int:
    """Calculate the per-leg distance (total distance divided by 4).

    Returns:
        The distance of a single leg in the relay.

    Raises:
        ValueError: If the event is an individual swim rather than a relay.
    """
    if not self.is_relay():
        raise ValueError(f"{self.name} is not a relay")
    return self.distance // 4
leg_strokes
leg_strokes() -> list[Stroke]

Get the ordered sequence of strokes swum on the four legs of this relay.

Returns:

Type Description
list[Stroke]

A list of four Stroke values representing each leg in order.

Raises:

Type Description
ValueError

If the event is an individual swim rather than a relay.

Source code in src/tunas/event.py
def leg_strokes(self) -> list[Stroke]:
    """Get the ordered sequence of strokes swum on the four legs of this relay.

    Returns:
        A list of four `Stroke` values representing each leg in order.

    Raises:
        ValueError: If the event is an individual swim rather than a relay.
    """
    if not self.is_relay():
        raise ValueError(f"{self.name} is not a relay")
    if self.stroke is _MEDLEY_R:
        return list(_MEDLEY_LEG_STROKES)
    return list(_FREE_LEG_STROKES)
leg_event
leg_event(order: int) -> Event

Resolve the individual Event swum on a specific relay leg.

Parameters:

Name Type Description Default
order int

The 1-based leg position (must be between 1 and 4).

required

Returns:

Type Description
Event

The corresponding individual Event swum on that leg.

Raises:

Type Description
ValueError

If this is an individual event, or if order is not between 1 and 4 inclusive.

Source code in src/tunas/event.py
def leg_event(self, order: int) -> Event:
    """Resolve the individual `Event` swum on a specific relay leg.

    Args:
        order: The 1-based leg position (must be between 1 and 4).

    Returns:
        The corresponding individual `Event` swum on that leg.

    Raises:
        ValueError: If this is an individual event, or if `order` is not
            between 1 and 4 inclusive.
    """
    if not self.is_relay():
        raise ValueError(f"{self.name} is not a relay")
    if not 1 <= order <= 4:
        raise ValueError(f"Relay leg order must be 1-4, got {order}")
    leg_stroke = self.leg_strokes()[order - 1]
    event = Event.find(self.leg_distance(), leg_stroke, self.course)
    if event is None:  # pragma: no cover - every defined relay has real legs
        raise ValueError(f"No individual event for leg {order} of {self.name}")
    return event

time

Immutable swim time value type with centisecond precision.

Time dataclass

Time(centiseconds: int)

An immutable, hashable swim time with centisecond precision.

Stored internally as a non-negative centiseconds integer. Faster times compare as less than slower times (i.e. smaller values indicate faster swims). Supports addition and subtraction between Time instances.

String formatting returns "M:SS.HH" if the time is at least one minute, or "SS.HH" if under a minute. The formatted string zero-pads seconds and hundredths but does not pad the minute component.

Attributes:

Name Type Description
centiseconds int

Duration of the swim in hundredths of a second.

minute property
minute: int

Whole-minute component (centiseconds // 6000).

second property
second: int

Seconds within the minute (0-59).

hundredth property
hundredth: int

Hundredths-of-a-second component (0-99).

total_seconds property
total_seconds: float

The whole time expressed as seconds (centiseconds / 100).

parse classmethod
parse(s: str) -> Time

Parse a swim-time string formatted as [M:]SS.HH (the minutes part is optional).

Strips leading/trailing whitespace. Tolerates 1- or 2-digit minutes and seconds; fractions of a second are taken to centisecond precision, with any extra digits rounded to the nearest hundredth (e.g. "1:04.875" -> 1:04.88).

Parameters:

Name Type Description Default
s str

The time string to parse (e.g., "1:04.87", "23.4", "8.23").

required

Returns:

Type Description
Time

A new Time instance with the parsed centiseconds duration.

Raises:

Type Description
ValueError

If the string is empty, lacks a decimal point, contains non-numeric digits, or has more than two segments separated by colons.

Source code in src/tunas/time.py
@classmethod
def parse(cls, s: str) -> Time:
    """Parse a swim-time string formatted as `[M:]SS.HH` (the minutes part is optional).

    Strips leading/trailing whitespace. Tolerates 1- or 2-digit minutes and
    seconds; fractions of a second are taken to centisecond precision, with
    any extra digits rounded to the nearest hundredth (e.g. ``"1:04.875"`` ->
    ``1:04.88``).

    Args:
        s: The time string to parse (e.g., `"1:04.87"`, `"23.4"`, `"8.23"`).

    Returns:
        A new `Time` instance with the parsed centiseconds duration.

    Raises:
        ValueError: If the string is empty, lacks a decimal point, contains
            non-numeric digits, or has more than two segments separated by colons.
    """
    raw = s.strip()
    if not raw:
        raise ValueError(f"Invalid time string: {s!r}")

    parts = raw.split(":")
    if len(parts) == 1:
        minutes_str, sec_part = "0", parts[0]
    elif len(parts) == 2:
        minutes_str, sec_part = parts
    else:
        raise ValueError(f"Invalid time string: {s!r}")

    if "." not in sec_part:
        raise ValueError(f"Invalid time string: {s!r}")
    seconds_str, frac_str = sec_part.split(".", 1)

    if not (minutes_str.isdigit() and seconds_str.isdigit() and frac_str.isdigit()):
        raise ValueError(f"Invalid time string: {s!r}")

    minutes = int(minutes_str)
    seconds = int(seconds_str)
    if len(frac_str) <= 2:
        hundredths = int((frac_str + "00")[:2])  # 1- or 2-digit fraction: exact
    else:
        # Round any extra precision to the nearest hundredth; a value that
        # rounds up to 100 carries cleanly since we store total centiseconds.
        hundredths = round(int(frac_str) / 10 ** len(frac_str) * 100)
    return cls(minutes * 6000 + seconds * 100 + hundredths)