Coverage for backend/django/core/auxiliary/services/result_summary/aggregation.py: 85%
47 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
1"""Reusable numeric aggregation helpers for MSS result-derived values."""
3from __future__ import annotations
5from decimal import Decimal
6from enum import StrEnum
7from typing import Iterable
10class NumericAggregateFunction(StrEnum):
11 """Supported scalar reductions for schedule and result-summary values."""
13 MIN = "min"
14 MAX = "max"
15 MEAN = "mean"
16 PERCENTILE = "percentile"
19def aggregate_values(
20 values: Iterable[Decimal | float | int | str],
21 aggregate_function: str,
22 *,
23 percentile: Decimal | float | int | str | None = None,
24) -> Decimal:
25 """Aggregate finite numeric values using the shared MSS reduction semantics."""
27 numeric_values = _decimal_values(values)
28 if not numeric_values: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true
29 raise ValueError("At least one finite value is required.")
30 function = NumericAggregateFunction(str(aggregate_function or "").strip().lower())
31 if function == NumericAggregateFunction.MIN:
32 return min(numeric_values)
33 if function == NumericAggregateFunction.MAX:
34 return max(numeric_values)
35 if function == NumericAggregateFunction.MEAN:
36 return sum(numeric_values, Decimal("0")) / Decimal(len(numeric_values))
37 if function == NumericAggregateFunction.PERCENTILE: 37 ↛ 40line 37 didn't jump to line 40 because the condition on line 37 was always true
38 percentile_value = Decimal("50") if percentile is None else _decimal_value(percentile)
39 return linear_percentile(numeric_values, percentile_value)
40 raise ValueError(f"Unsupported aggregate function: {aggregate_function}")
43def linear_percentile(
44 values: Iterable[Decimal | float | int | str],
45 percentile: Decimal | float | int | str,
46) -> Decimal:
47 """Return a percentile using linear interpolation between sorted values."""
49 numeric_values = sorted(_decimal_values(values))
50 if not numeric_values: 50 ↛ 51line 50 didn't jump to line 51 because the condition on line 50 was never true
51 raise ValueError("At least one finite value is required.")
52 percentile_value = _decimal_value(percentile)
53 if percentile_value < 0 or percentile_value > 100: 53 ↛ 54line 53 didn't jump to line 54 because the condition on line 53 was never true
54 raise ValueError("Percentile must be between 0 and 100.")
55 if len(numeric_values) == 1:
56 return numeric_values[0]
57 rank = (percentile_value / Decimal("100")) * Decimal(len(numeric_values) - 1)
58 lower_index = int(rank)
59 upper_index = min(lower_index + 1, len(numeric_values) - 1)
60 fraction = rank - Decimal(lower_index)
61 lower = numeric_values[lower_index]
62 upper = numeric_values[upper_index]
63 return lower + ((upper - lower) * fraction)
66def _decimal_values(values: Iterable[Decimal | float | int | str]) -> list[Decimal]:
67 return [_decimal_value(value) for value in values]
70def _decimal_value(value: Decimal | float | int | str) -> Decimal:
71 decimal_value = Decimal(str(value))
72 if not decimal_value.is_finite(): 72 ↛ 73line 72 didn't jump to line 73 because the condition on line 72 was never true
73 raise ValueError("Aggregate values must be finite.")
74 return decimal_value