fix(analysis): _expected_ticks usa ceiling division (no off-by-one)

Il piano originale aveva `floor(span/15) + 1` che over-conta a span allineati
(span=60min → 5 invece di 4). Il primo fix dell'implementer (`floor(span/15)`)
under-conta a span non-allineati (span=16min → 1 invece di 2). Solo
`ceil(span/15)` è corretto in entrambi i casi. Aggiunti 2 test che
coprono gli scenari non-allineato e boundary-esatto per impedire
regressioni. Plan doc allineato.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-05-13 09:28:24 +00:00
parent 35ac92e938
commit 9e2216d202
3 changed files with 30 additions and 4 deletions
@@ -246,8 +246,12 @@ def _expected_ticks(since: datetime, until: datetime) -> int:
)
if first_tick >= until:
return 0
span = until - first_tick
return int(span.total_seconds() // (_TICK_INTERVAL_MIN * 60)) + 1
# Count ticks in [first_tick, until): the count is
# ceil(span_minutes / 15). floor() under-counts at non-multiple
# spans; floor()+1 over-counts at aligned multiples. Only
# ceil() works for both. Requires `import math` at the top.
span_seconds = (until - first_tick).total_seconds()
return math.ceil(span_seconds / (_TICK_INTERVAL_MIN * 60))
```
- [ ] **Step 4: Run tests to verify they pass**