Début de journée indépendant de l'ordre des créneaux types
La « journée de festival » commence désormais au début du plus long creux laissé par les créneaux types sur 24 h (l'heure où le dernier se termine), et non plus au début de celui de plus petit `order` : un créneau type d'ordre 0 déplacé à 21:00 faisait commencer chaque journée des grilles à 21:00, jusqu'à 03:00 le lendemain. Même règle côté backend (validation.engine.day_boundary, réutilisée par la génération de créneaux) et frontend (dayBoundaryFromDayParts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ee1f6ed72e
commit
f671bb85bf
@@ -5,6 +5,7 @@ import datetime as dt
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.models import DayPart, Edition, Position, Shift
|
||||
from app.validation.engine import day_boundary
|
||||
|
||||
|
||||
def _dates(edition: Edition) -> list[dt.date]:
|
||||
@@ -12,17 +13,6 @@ def _dates(edition: Edition) -> list[dt.date]:
|
||||
return [edition.start_date + dt.timedelta(days=i) for i in range(span + 1)]
|
||||
|
||||
|
||||
def _day_boundary(day_parts: list[DayPart]) -> dt.time:
|
||||
"""Début du premier créneau type du jour (le plus petit `order`) — même
|
||||
frontière que `validation.engine._day_boundary`. Un créneau type dont
|
||||
l'heure de début tombe avant cette frontière est la suite de la nuit du
|
||||
jour précédent (ex. `Nuit3` 00 h–01 h après `Soirée` 19 h–00 h) : il doit
|
||||
être généré sur le jour *suivant*, pas sur `day` lui-même."""
|
||||
if not day_parts:
|
||||
return dt.time(0, 0)
|
||||
return min(day_parts, key=lambda part: part.order).start_time
|
||||
|
||||
|
||||
def _window(day: dt.date, part: DayPart, boundary: dt.time) -> tuple[dt.datetime, dt.datetime]:
|
||||
actual_day = day + dt.timedelta(days=1) if part.start_time < boundary else day
|
||||
start = dt.datetime.combine(actual_day, part.start_time)
|
||||
@@ -40,7 +30,7 @@ def generate_missing_shifts(
|
||||
day_parts = list(
|
||||
session.exec(select(DayPart).where(DayPart.edition_id == edition.id))
|
||||
)
|
||||
boundary = _day_boundary(day_parts)
|
||||
boundary = day_boundary(day_parts)
|
||||
existing = {
|
||||
(s.day_part_id, s.starts_at.date())
|
||||
for s in session.exec(
|
||||
|
||||
@@ -107,14 +107,50 @@ def _check_outside_availability(data: ValidationInput) -> list[Violation]:
|
||||
# --------------------------------------------------------------------------- #
|
||||
# DAILY_HOURS_EXCEEDED (avertissement) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
_MINUTES_PER_DAY = 24 * 60
|
||||
|
||||
|
||||
def day_boundary(day_parts) -> dt.time:
|
||||
"""Début d'une « journée de festival » : début du premier créneau type
|
||||
(`DayPart`, par `order`), minuit à défaut — voir CLAUDE.md § 3. Accepte
|
||||
aussi bien les `DayPartInfo` de ce module que les lignes `DayPart`."""
|
||||
"""Début d'une « journée de festival » : début du plus long creux que
|
||||
laissent les créneaux types (`DayPart`) sur le cadran de 24 h — l'heure à
|
||||
laquelle le dernier de la journée se termine —, minuit à défaut — voir
|
||||
CLAUDE.md § 3. Avec Matin 08 h–13 h, Après-midi 13 h–19 h, Soirée
|
||||
19 h–02 h, le creux 02 h–08 h est le plus long : la journée
|
||||
commence à 02 h, la soirée qui passe minuit reste sur le jour où elle a
|
||||
commencé. Indépendant de `order`, qui n'est qu'un ordre d'affichage et
|
||||
ne suit pas les heures quand on les modifie (un créneau type d'ordre 0
|
||||
déplacé à 21 h faisait commencer chaque journée à 21 h). Sans creux (les
|
||||
créneaux types couvrent 24 h), début du premier par `order`. Accepte
|
||||
aussi bien les `DayPartInfo` de ce module que les lignes `DayPart`.
|
||||
Miroir front : `lib/planning.ts:dayBoundaryFromDayParts`."""
|
||||
day_parts = list(day_parts)
|
||||
if not day_parts:
|
||||
return dt.time(0, 0)
|
||||
return min(day_parts, key=lambda part: part.order).start_time
|
||||
covered = [False] * _MINUTES_PER_DAY
|
||||
for part in day_parts:
|
||||
start = part.start_time.hour * 60 + part.start_time.minute
|
||||
end = part.end_time.hour * 60 + part.end_time.minute
|
||||
length = (end - start) % _MINUTES_PER_DAY or _MINUTES_PER_DAY
|
||||
for offset in range(length):
|
||||
covered[(start + offset) % _MINUTES_PER_DAY] = True
|
||||
if all(covered):
|
||||
return min(day_parts, key=lambda part: part.order).start_time
|
||||
# Parcours circulaire à partir d'une minute couverte : chaque creux est
|
||||
# vu d'un seul tenant, même s'il enjambe minuit.
|
||||
origin = covered.index(True)
|
||||
best_start, best_length = 0, 0
|
||||
run_start, run_length = 0, 0
|
||||
for offset in range(1, _MINUTES_PER_DAY + 1):
|
||||
minute = (origin + offset) % _MINUTES_PER_DAY
|
||||
if covered[minute]:
|
||||
run_length = 0
|
||||
continue
|
||||
if run_length == 0:
|
||||
run_start = minute
|
||||
run_length += 1
|
||||
if run_length > best_length:
|
||||
best_start, best_length = run_start, run_length
|
||||
return dt.time(best_start // 60, best_start % 60)
|
||||
|
||||
|
||||
def festival_day(moment: dt.datetime, boundary: dt.time) -> dt.date:
|
||||
|
||||
Reference in New Issue
Block a user