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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## [Unreleased]

### Fixed

- `Match.team_1_run_rate`, `Match.team_2_run_rate` and each innings'
`run_rate` are right for an innings that ends mid-over. ESPN writes 19.4 for
19 overs and 4 balls, and the rate divided runs by that figure, so 159 runs
came out at 8.2 an over instead of 8.08. The rate now counts balls, using
ESPN's `ballsPerOver` where overs are not six balls.

## [1.1.0] - 2026-10-23

### Fixed
Expand Down
25 changes: 21 additions & 4 deletions espncricinfo/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,26 @@ def _boundary_count(inn: dict[str, Any], key: str) -> int | None:
return sum(counts)


def _run_rate(inn: dict[str, Any]) -> float:
"""Runs per over for an innings; 0.0 before a ball is bowled.

ESPN writes overs in cricket notation, where 19.4 is 19 overs and 4 balls,
so dividing runs by it overstates the rate of an innings that ends
mid-over. Count balls instead: ESPN's ``balls``, else the overs figure
converted with ``ballsPerOver`` (6 unless ESPN says otherwise).
"""
try:
runs = inn.get("runs") or 0
per_over = inn.get("ballsPerOver") or 6
balls = inn.get("balls")
if not balls:
overs = float(inn.get("overs") or 0)
balls = int(overs) * per_over + round(overs % 1 * 10)
return round(runs * per_over / balls, 2) if balls > 0 else 0.0
except (TypeError, ValueError, ZeroDivisionError):
return 0.0


def _as_float(record: dict[str, Any] | None, key: str) -> float | None:
"""Return ``record[key]`` as a float, or None if absent or not numeric."""
if record is None:
Expand Down Expand Up @@ -183,10 +203,7 @@ def build_team(t):
for inn in innings_raw:
overs = inn.get("overs") or 0
runs = inn.get("runs") or 0
try:
run_rate = round(runs / overs, 2) if overs > 0 else 0.0
except (ZeroDivisionError, TypeError):
run_rate = 0.0
run_rate = _run_rate(inn)
innings_normalized.append(
{
"batting_team_id": str(inn["team"]["objectId"]),
Expand Down
37 changes: 37 additions & 0 deletions tests/test_match.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,43 @@ def test_boundary_count_falls_back_to_batters_then_none():
assert _boundary_count({"inningBatsmen": []}, "fours") is None


def test_run_rate_counts_a_part_over_in_balls():
"""ESPN's 19.4 overs is 19 overs and 4 balls, not 19.4 overs."""
from espncricinfo.match import _run_rate

chase = {"runs": 159, "overs": 19.4, "balls": 118, "ballsPerOver": 6}
assert _run_rate(chase) == 8.08 # 159 / 19.4 would give 8.2
assert _run_rate({"runs": 176, "overs": 20, "balls": 120}) == 8.8


def test_run_rate_converts_overs_when_espn_gives_no_ball_count():
from espncricinfo.match import _run_rate

assert _run_rate({"runs": 159, "overs": 19.4}) == 8.08
assert _run_rate({"runs": 159, "overs": 19.4, "balls": None}) == 8.08
# Eight-ball overs, as in older Tests: 12.4 overs is 100 balls.
assert _run_rate({"runs": 100, "overs": 12.4, "ballsPerOver": 8}) == 8.0


def test_run_rate_is_zero_before_a_ball_is_bowled():
from espncricinfo.match import _run_rate

assert _run_rate({"runs": 0, "overs": 0, "balls": 0}) == 0.0
assert _run_rate({"runs": None, "overs": None}) == 0.0
assert _run_rate({}) == 0.0


def test_team_run_rate_for_an_innings_that_ends_mid_over():
next_data = _next_data_with()
app = next_data["props"]["appPageProps"]["data"]
data = app if "match" in app else app["data"]
data["content"]["innings"][1].update(overs=19.4, balls=118)
match = _match_from(next_data)
assert match.team_2_overs_batted == 19.4
assert match.team_2_run_rate == 8.08
assert match.team_1_run_rate == 8.8


def test_over_by_over_adds_up_to_innings_totals():
match = _make_match()
assert [len(overs) for overs in match.over_by_over] == [20, 20]
Expand Down