feat: monthly_stats() — médiane journalière et hebdomadaire
This commit is contained in:
@@ -25,6 +25,30 @@ def week_balance_minutes(actual_minutes: int, reference_minutes: int) -> int:
|
|||||||
return actual_minutes - reference_minutes
|
return actual_minutes - reference_minutes
|
||||||
|
|
||||||
|
|
||||||
|
import statistics as _stats
|
||||||
|
|
||||||
|
|
||||||
|
def monthly_stats(entries: list) -> dict:
|
||||||
|
"""
|
||||||
|
Calcule médiane journalière et médiane hebdomadaire (semaines ISO)
|
||||||
|
pour un groupe d'entrées. Les absences (total_minutes=0) sont incluses.
|
||||||
|
"""
|
||||||
|
if not entries:
|
||||||
|
return {"median_daily_min": 0, "median_weekly_min": 0}
|
||||||
|
|
||||||
|
daily = [e.total_minutes() for e in entries]
|
||||||
|
median_daily = int(_stats.median(daily))
|
||||||
|
|
||||||
|
weekly: dict[tuple, int] = {}
|
||||||
|
for e in entries:
|
||||||
|
key = e.date.isocalendar()[:2] # (year, isoweek)
|
||||||
|
weekly[key] = weekly.get(key, 0) + e.total_minutes()
|
||||||
|
|
||||||
|
median_weekly = int(_stats.median(weekly.values()))
|
||||||
|
|
||||||
|
return {"median_daily_min": median_daily, "median_weekly_min": median_weekly}
|
||||||
|
|
||||||
|
|
||||||
def count_day_types(entries: list) -> dict[str, int]:
|
def count_day_types(entries: list) -> dict[str, int]:
|
||||||
"""Retourne un dict {day_type: count} pour une liste d'entrées, sans les zéros."""
|
"""Retourne un dict {day_type: count} pour une liste d'entrées, sans les zéros."""
|
||||||
counts: dict[str, int] = {}
|
counts: dict[str, int] = {}
|
||||||
|
|||||||
@@ -43,8 +43,9 @@ def test_week_balance_negative():
|
|||||||
|
|
||||||
|
|
||||||
from app.business.time_calc import count_day_types
|
from app.business.time_calc import count_day_types
|
||||||
from app.models import WorkEntry
|
from app.models import WorkEntry, TimeSlot
|
||||||
from datetime import date
|
from app.business.time_calc import monthly_stats
|
||||||
|
from datetime import date, time as dtime
|
||||||
|
|
||||||
|
|
||||||
def test_count_day_types_basic():
|
def test_count_day_types_basic():
|
||||||
@@ -62,6 +63,63 @@ def test_count_day_types_empty():
|
|||||||
assert count_day_types([]) == {}
|
assert count_day_types([]) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def _entry(d: date, *slots: tuple[str, str]) -> WorkEntry:
|
||||||
|
"""Helper : crée un WorkEntry avec des TimeSlots."""
|
||||||
|
entry = WorkEntry(date=d, day_type="WORK")
|
||||||
|
entry.time_slots = [
|
||||||
|
TimeSlot(start_time=dtime(*[int(x) for x in s.split(":")]),
|
||||||
|
end_time=dtime(*[int(x) for x in e.split(":")]))
|
||||||
|
for s, e in slots
|
||||||
|
]
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def _absence(d: date) -> WorkEntry:
|
||||||
|
"""Helper : crée un WorkEntry sans time_slots (0 min)."""
|
||||||
|
entry = WorkEntry(date=d, day_type="CONGE")
|
||||||
|
entry.time_slots = []
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def test_monthly_stats_empty():
|
||||||
|
result = monthly_stats([])
|
||||||
|
assert result == {"median_daily_min": 0, "median_weekly_min": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_monthly_stats_median_daily_odd():
|
||||||
|
# 420, 465, 510 → médiane = 465
|
||||||
|
entries = [
|
||||||
|
_entry(date(2025, 1, 6), ("9:00", "16:00")), # 420 min
|
||||||
|
_entry(date(2025, 1, 7), ("9:00", "16:45")), # 465 min
|
||||||
|
_entry(date(2025, 1, 8), ("9:00", "17:30")), # 510 min
|
||||||
|
]
|
||||||
|
result = monthly_stats(entries)
|
||||||
|
assert result["median_daily_min"] == 465
|
||||||
|
|
||||||
|
|
||||||
|
def test_monthly_stats_includes_absences():
|
||||||
|
# 0, 465 → médiane de 2 valeurs = (0+465)/2 = 232 (int)
|
||||||
|
entries = [
|
||||||
|
_absence(date(2025, 1, 6)),
|
||||||
|
_entry(date(2025, 1, 7), ("9:00", "16:45")),
|
||||||
|
]
|
||||||
|
result = monthly_stats(entries)
|
||||||
|
assert result["median_daily_min"] == 232
|
||||||
|
|
||||||
|
|
||||||
|
def test_monthly_stats_median_weekly():
|
||||||
|
# Semaine 1 : 465+465 = 930 min
|
||||||
|
# Semaine 2 : 420 min
|
||||||
|
# médiane([930, 420]) = (420+930)/2 = 675
|
||||||
|
entries = [
|
||||||
|
_entry(date(2025, 1, 6), ("9:00", "16:45")), # sem 2
|
||||||
|
_entry(date(2025, 1, 7), ("9:00", "16:45")), # sem 2
|
||||||
|
_entry(date(2025, 1, 13), ("9:00", "16:00")), # sem 3
|
||||||
|
]
|
||||||
|
result = monthly_stats(entries)
|
||||||
|
assert result["median_weekly_min"] == 675
|
||||||
|
|
||||||
|
|
||||||
def test_count_day_types_single_type():
|
def test_count_day_types_single_type():
|
||||||
entries = [
|
entries = [
|
||||||
WorkEntry(date=date(2025, 2, 1), day_type="RTT"),
|
WorkEntry(date=date(2025, 2, 1), day_type="RTT"),
|
||||||
|
|||||||
Reference in New Issue
Block a user