Skip to content
Merged
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
38 changes: 38 additions & 0 deletions src/pyfantastat/championship.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,44 @@ def generate_ranking(self) -> None:
self.teams[match[0]].add_goals(goals[0], goals[1])
self.teams[match[1]].add_goals(goals[1], goals[0])

def ranking_at_matchday(
self,
matchday: int,
sort: bool = False,
) -> "np.ndarray | tuple[np.ndarray, np.ndarray]":
"""Return league points at a specific matchday without mutating object state.

:param matchday: Zero-based index of the matchday to evaluate (inclusive).
Pass ``0`` to get the state before any match is played.
:param sort: If ``True``, also return a sorted-indices array (descending
by points). For full tiebreaker resolution use
``set_current_matchday`` + ``generate_ranking`` + ``sort_ranking``.
:returns: ``ranking`` array when *sort* is ``False``; ``(ranking,
sorted_indices)`` when *sort* is ``True``.
:raises ValueError: If calendar is not set or *matchday* is out of range.
"""
if not self.calendar:
raise ValueError("Calendar must be set before generating ranking.")
if matchday < 0 or matchday > len(self.calendar):
raise ValueError(f"Matchday index {matchday} is out of range.")

ranking = np.zeros(len(self.teams))
for day_idx, day in enumerate(self.calendar[:matchday]):
for match in day:
res, _ = self.match_result(match, day_idx)
if res == 1:
ranking[match[0]] += 3
elif res == 2:
ranking[match[1]] += 3
else:
ranking[match[0]] += 1
ranking[match[1]] += 1

if sort:
sorted_indices = np.argsort(-ranking, kind="stable")
return ranking, sorted_indices
return ranking

# ------------------------------------------------------------------
# Ranking sorting / tiebreakers
# ------------------------------------------------------------------
Expand Down
74 changes: 74 additions & 0 deletions tests/test_championship.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,77 @@ def test_generate_ranking_idempotent(simple_championship):

assert list(simple_championship.ranking) == list(ranking_first)
assert goals_scored_first == goals_scored_second


# ---------------------------------------------------------------------------
# ranking_at_matchday tests
# ---------------------------------------------------------------------------


def test_ranking_at_matchday_zero(simple_championship):
"""Before any match, all teams have 0 points."""
result = simple_championship.ranking_at_matchday(0)
assert np.all(result == 0)
assert result.shape == (4,)


def test_ranking_at_matchday_partial(simple_championship):
"""Points after matchday 1 match a manual re-run up to that point."""
result = simple_championship.ranking_at_matchday(1)
# round 0: T0(80) vs T1(70) → T0 wins; T2(75) vs T3(66) → T2 wins
assert result[0] == 3
assert result[2] == 3
assert result[1] == 0
assert result[3] == 0


def test_ranking_at_matchday_full(simple_championship):
"""At current_matchday, result matches self.ranking after generate_ranking()."""
simple_championship.set_current_matchday(3)
simple_championship.generate_ranking()
expected = simple_championship.ranking.copy()

result = simple_championship.ranking_at_matchday(3)
np.testing.assert_array_equal(result, expected)


def test_ranking_at_matchday_does_not_mutate(simple_championship):
"""ranking_at_matchday must not modify self.ranking or team goals."""
simple_championship.generate_ranking()
ranking_before = simple_championship.ranking.copy()
goals_before = [t.goals_scored for t in simple_championship.teams]

simple_championship.ranking_at_matchday(2)

np.testing.assert_array_equal(simple_championship.ranking, ranking_before)
assert [t.goals_scored for t in simple_championship.teams] == goals_before


def test_ranking_at_matchday_sort_flag(simple_championship):
"""sort=True returns (ranking, sorted_indices) with descending order."""
ranking, sorted_indices = simple_championship.ranking_at_matchday(3, sort=True)
assert isinstance(ranking, np.ndarray)
assert isinstance(sorted_indices, np.ndarray)
# sorted_indices must produce a non-increasing sequence of points
assert all(
ranking[sorted_indices[i]] >= ranking[sorted_indices[i + 1]]
for i in range(len(sorted_indices) - 1)
)
# first entry is the team with the most points
assert ranking[sorted_indices[0]] == ranking.max()


def test_ranking_at_matchday_out_of_range(simple_championship):
"""Negative or too-large matchday raises ValueError."""
with pytest.raises(ValueError):
simple_championship.ranking_at_matchday(-1)
with pytest.raises(ValueError):
simple_championship.ranking_at_matchday(999)


def test_ranking_at_matchday_no_calendar():
"""Raises ValueError when calendar is not set."""
teams = [Team(f"T{i}", f"U{i}") for i in range(4)]
ch = Championship(teams)
with pytest.raises(ValueError):
ch.ranking_at_matchday(0)
Loading