"""
Conflict detection module for family calendar events.
Detects when events share the same day and have overlapping family members.
"""
from datetime import datetime
from typing import List, Dict, Any, Optional
def parse_date(date_str: str) -> datetime:
"""Parse a date string (YYYY-MM-DD) into a datetime object."""
return datetime.strptime(date_str, "%Y-%m-%d")
def get_day_label(start_date: str, reference_date: Optional[datetime] = None) -> str:
"""
Compute a dynamic day label from a start date.
Returns:
- "Today" if the date is today
- "Tomorrow" if the date is tomorrow
- "Mon May 05" format for other dates
"""
if reference_date is None:
reference_date = datetime.now()
event_date = parse_date(start_date)
# Strip time for day comparison
ref_date = reference_date.replace(hour=0, minute=0, second=0, microsecond=0)
evt_date = event_date.replace(hour=0, minute=0, second=0, microsecond=0)
delta_days = (evt_date - ref_date).days
if delta_days == 0:
return "Today"
elif delta_days == 1:
return "Tomorrow"
else:
return evt_date.strftime("%a %b %d")
def same_day(date1: str, date2: str) -> bool:
"""Check if two date strings represent the same day."""
d1 = parse_date(date1)
d2 = parse_date(date2)
return d1.date() == d2.date()
def has_shared_members(event1: Dict[str, Any], event2: Dict[str, Any]) -> bool:
"""
Check if two events share any family members.
If either event lacks family_members field, returns False (no person conflict).
"""
members1 = event1.get("family_members", [])
members2 = event2.get("family_members", [])
# If either has no family members defined, no person-level conflict
if not members1 or not members2:
return False
set1 = set(members1)
set2 = set(members2)
return bool(set1 & set2)
def find_conflicts(event: Dict[str, Any], all_events: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Find all events that conflict with the given event.
Conflict rules:
1. Events must be on the same day (naive date-only check for now)
2. Events must NOT be the same event (different IDs)
3. If both events have family_members, they must share at least one member
4. If neither has family_members, it's a same-day conflict (fallback)
Args:
event: The event to check for conflicts
all_events: List of all events to check against
Returns:
List of conflicting events (minimal dict with id, title, family_members)
"""
conflicts = []
event_id = event.get("id")
event_date = event.get("start")
event_members = event.get("family_members", [])
if not event_date:
return conflicts
for other_event in all_events:
other_id = other_event.get("id")
other_date = other_event.get("start")
other_members = other_event.get("family_members", [])
# Skip self
if other_id == event_id:
continue
# Skip if no date
if not other_date:
continue
# Check same day
if not same_day(event_date, other_date):
continue
# Determine conflict type
if event_members and other_members:
# Both have family members - check for shared members
if has_shared_members(event, other_event):
conflicts.append({
"id": other_id,
"title": other_event.get("title", "Untitled"),
"family_members": other_members,
"conflict_type": "person" # Same person, same day
})
elif event_members or other_members:
# One has family members, one doesn't - only same-day conflict
# This is a "same-day" conflict (yellow warning)
conflicts.append({
"id": other_id,
"title": other_event.get("title", "Untitled"),
"family_members": other_members,
"conflict_type": "same_day"
})
else:
# Neither has family members - same-day conflict only
conflicts.append({
"id": other_id,
"title": other_event.get("title", "Untitled"),
"family_members": [],
"conflict_type": "same_day"
})
return conflicts
def add_conflict_data(event: Dict[str, Any], all_events: List[Dict[str, Any]], reference_date: Optional[datetime] = None) -> Dict[str, Any]:
"""
Enrich an event with conflict data and dynamic day label.
Args:
event: The event to enrich
all_events: List of all events for conflict detection
reference_date: Optional reference date for day label calculation
Returns:
Event dict with added 'conflicts' array and computed 'day_label'
"""
enriched = event.copy()
# Compute dynamic day label
start_date = event.get("start", "")
if start_date:
enriched["day_label"] = get_day_label(start_date, reference_date)
# Find conflicts
enriched["conflicts"] = find_conflicts(event, all_events)
return enriched
def add_conflicts_to_all_events(events: List[Dict[str, Any]], reference_date: Optional[datetime] = None) -> List[Dict[str, Any]]:
"""
Process all events, adding conflict data and dynamic day labels to each.
Args:
events: List of all events
reference_date: Optional reference date for day label calculation
Returns:
List of enriched event dicts
"""
return [add_conflict_data(event, events, reference_date) for event in events]