feat(analysis): _expected_ticks per finestre */15 allineate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -93,3 +93,29 @@ class ChainAuditReport:
|
|||||||
iv_null_count: int = 0
|
iv_null_count: int = 0
|
||||||
iv_null_pct: Decimal = Decimal("0")
|
iv_null_pct: Decimal = Decimal("0")
|
||||||
depth_zero_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))
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user