"""Display-only merge of living candidate intervals. Decision width stays on ``interval_union_width`` / ``unionWindowIntervals``. This module is not part of the sealed scoring identity: scoring must not import it. """ from __future__ import annotations from datetime import datetime from typing import Any LIVING_DISPLAY_MERGE_GAP_MINUTES = 4 LIVING_DISPLAY_MAX_SEGMENTS = 3 def _stamp(value: str) -> datetime: return datetime.fromisoformat(value) def merge_living_display_intervals( intervals: list[dict[str, Any]], gap_minutes: int = LIVING_DISPLAY_MERGE_GAP_MINUTES, ) -> list[dict[str, str]]: """Merge intervals whose gap is <= gap_minutes. Gap is the open minutes between them.""" rows = [] for row in intervals: start, end = str(row.get("start_at") or ""), str(row.get("end_at") or "") if len(start) < 16 or len(end) < 16 or start > end: continue try: _stamp(start) _stamp(end) except ValueError: continue rows.append({"start_at": start[:16], "end_at": end[:16]}) rows.sort(key=lambda item: (item["start_at"], item["end_at"])) merged: list[dict[str, str]] = [] for row in rows: if not merged: merged.append(row) continue previous = merged[-1] gap = int((_stamp(row["start_at"]) - _stamp(previous["end_at"])).total_seconds() // 60) - 1 if gap <= gap_minutes: if row["end_at"] > previous["end_at"]: previous["end_at"] = row["end_at"] else: merged.append(row) return merged def inclusive_interval_minutes(intervals: list[dict[str, str]]) -> int: total = 0 for row in intervals: width = int((_stamp(row["end_at"]) - _stamp(row["start_at"])).total_seconds() // 60) + 1 if width > 0: total += width return total def living_display_label( intervals: list[dict[str, str]], birth_date: str | None = None, max_segments: int = LIVING_DISPLAY_MAX_SEGMENTS, ) -> str: def label(value: str) -> str: date, clock = value[:10], value[11:16] if not birth_date: return f"{date} {clock}" if date == birth_date: return clock return f"{'前一天' if date < birth_date else '后一天'} {clock}" def text(row: dict[str, str]) -> str: return f"{label(row['start_at'])}–{label(row['end_at'])}" if len(intervals) <= max_segments: return "、".join(text(row) for row in intervals) shown = "、".join(text(row) for row in intervals[:max_segments]) return f"{shown},等 {len(intervals)} 段,共约 {inclusive_interval_minutes(intervals)} 分钟"