From 35ac92e9384e5ef77a9c8c4b0043246da3e50d61 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 12 May 2026 22:32:12 +0000 Subject: [PATCH] feat(analysis): _expected_ticks per finestre */15 allineate Co-Authored-By: Claude Sonnet 4.6 --- src/cerbero_bite/analysis/data_audit.py | 26 +++++++++++++++++++++++ tests/unit/test_data_audit.py | 28 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tests/unit/test_data_audit.py diff --git a/src/cerbero_bite/analysis/data_audit.py b/src/cerbero_bite/analysis/data_audit.py index 69a98b5..25be057 100644 --- a/src/cerbero_bite/analysis/data_audit.py +++ b/src/cerbero_bite/analysis/data_audit.py @@ -93,3 +93,29 @@ class ChainAuditReport: iv_null_count: int = 0 iv_null_pct: Decimal = Decimal("0") depth_zero_pct: Decimal = Decimal("0") + + +def _expected_ticks(since: datetime, until: datetime) -> int: + """Number of `*/15` ticks in ``[since, until)`` aligned to wall clock. + + A tick is any UTC instant where ``minute % 15 == 0``. The first + tick at or after ``since`` is computed by rounding ``since`` up; + every subsequent tick is +15 minutes. The window is half-open on + the right. + """ + if until <= since: + return 0 + # Round `since` up to the next */15 boundary. + minute = since.minute + remainder = minute % _TICK_INTERVAL_MIN + if remainder == 0 and since.second == 0 and since.microsecond == 0: + first_tick = since + else: + bump = _TICK_INTERVAL_MIN - remainder + first_tick = (since + timedelta(minutes=bump)).replace( + second=0, microsecond=0 + ) + if first_tick >= until: + return 0 + span = until - first_tick + return int(span.total_seconds() // (_TICK_INTERVAL_MIN * 60)) diff --git a/tests/unit/test_data_audit.py b/tests/unit/test_data_audit.py new file mode 100644 index 0000000..34e02cc --- /dev/null +++ b/tests/unit/test_data_audit.py @@ -0,0 +1,28 @@ +"""Unit tests for analysis.data_audit.""" + +from __future__ import annotations + +from datetime import UTC, datetime + +from cerbero_bite.analysis.data_audit import _expected_ticks # noqa: PLC2701 + + +def test_expected_ticks_basic() -> None: + since = datetime(2026, 5, 12, 12, 0, tzinfo=UTC) + until = datetime(2026, 5, 12, 13, 0, tzinfo=UTC) + # 12:00, 12:15, 12:30, 12:45 → 4 ticks before 13:00 + assert _expected_ticks(since, until) == 4 + + +def test_expected_ticks_inclusive_left_exclusive_right() -> None: + since = datetime(2026, 5, 12, 12, 0, tzinfo=UTC) + until = datetime(2026, 5, 12, 12, 15, tzinfo=UTC) + # Only 12:00 + assert _expected_ticks(since, until) == 1 + + +def test_expected_ticks_unaligned_since_rounds_up() -> None: + since = datetime(2026, 5, 12, 12, 7, tzinfo=UTC) + until = datetime(2026, 5, 12, 12, 30, tzinfo=UTC) + # First aligned tick after 12:07 is 12:15, then 12:30 is excluded + assert _expected_ticks(since, until) == 1