| |
| """Shared, dependency-free helpers for v25 evaluation tools.""" |
|
|
| from __future__ import annotations |
|
|
| import json |
| import math |
| from pathlib import Path |
| from typing import Any, Iterable |
|
|
|
|
| NOT_MEASURED = "not measured" |
|
|
|
|
| def read_json(path: Path) -> Any: |
| with path.open(encoding="utf-8") as handle: |
| return json.load(handle) |
|
|
|
|
| def read_jsonl(path: Path) -> list[dict[str, Any]]: |
| rows = [] |
| with path.open(encoding="utf-8") as handle: |
| for line_number, line in enumerate(handle, 1): |
| if not line.strip(): |
| continue |
| value = json.loads(line) |
| if not isinstance(value, dict): |
| raise ValueError(f"{path}:{line_number}: expected a JSON object") |
| rows.append(value) |
| return rows |
|
|
|
|
| def write_json(path: Path, value: Any) -> None: |
| path.parent.mkdir(parents=True, exist_ok=True) |
| temporary = path.with_suffix(path.suffix + ".tmp") |
| temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8") |
| temporary.replace(path) |
|
|
|
|
| def finite_number(value: Any) -> float | None: |
| if isinstance(value, bool) or not isinstance(value, (int, float)): |
| return None |
| number = float(value) |
| return number if math.isfinite(number) else None |
|
|
|
|
| def mean(values: Iterable[float]) -> float | None: |
| items = list(values) |
| return sum(items) / len(items) if items else None |
|
|
|
|
| def percentile(values: Iterable[float], fraction: float) -> float | None: |
| items = sorted(values) |
| if not items: |
| return None |
| index = (len(items) - 1) * fraction |
| low = int(index) |
| high = min(low + 1, len(items) - 1) |
| return items[low] + (items[high] - items[low]) * (index - low) |
|
|