"""Three-mode date intelligence engine for ProjectCraft.""" import re from datetime import date, timedelta def detect_date_mode(start: str, end: str, start_ai: bool, end_ai: bool) -> str: """ Detect which date mode to use. has_start = bool(start.strip()) and not start_ai has_end = bool(end.strip()) and not end_ai Mode A: both provided -> return "A" Mode B: start only -> return "B" Mode C: end only -> return "C" Neither: raise ValueError("At least one date must be provided.") """ has_start = bool(start.strip()) and not start_ai has_end = bool(end.strip()) and not end_ai if has_start and has_end: return "A" elif has_start: return "B" elif has_end: return "C" else: raise ValueError("At least one date must be provided.") def validate_date_order(start: str, end: str) -> bool: """Return True if start <= end (both as YYYY-MM-DD strings). Return True if either is empty.""" if not start or not end: return True parsed_start = parse_iso_date(start) parsed_end = parse_iso_date(end) if parsed_start is None or parsed_end is None: return True return parsed_start <= parsed_end def format_date_range(start: str, end: str) -> str: """Format two YYYY-MM-DD strings into a readable range like 'Jan 15, 2025 -> Mar 20, 2025'. Handle None/empty gracefully.""" def fmt(d: str) -> str: if not d: return "" parsed = parse_iso_date(d) if parsed is None: return "" # strftime('%d') zero-pads; lstrip('0') gives non-padded day cross-platform day = parsed.strftime("%d").lstrip("0") or "0" return parsed.strftime(f"%b {day}, %Y") start_fmt = fmt(start) if start else "" end_fmt = fmt(end) if end else "" if start_fmt and end_fmt: return f"{start_fmt} \u2192 {end_fmt}" elif start_fmt: return start_fmt elif end_fmt: return end_fmt return "" def parse_iso_date(date_str: str): """Parse a YYYY-MM-DD string to datetime.date. Return None if invalid.""" if not date_str or not isinstance(date_str, str): return None if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", date_str.strip()): return None try: parts = date_str.strip().split("-") return date(int(parts[0]), int(parts[1]), int(parts[2])) except (ValueError, IndexError): return None def days_between(start: str, end: str) -> int: """Return number of days between two YYYY-MM-DD strings. Return 0 if either is invalid.""" parsed_start = parse_iso_date(start) parsed_end = parse_iso_date(end) if parsed_start is None or parsed_end is None: return 0 return abs((parsed_end - parsed_start).days) def offset_date(base_date: str, offset_days: int) -> str: """Add offset_days to a YYYY-MM-DD string. Return the result as YYYY-MM-DD.""" parsed = parse_iso_date(base_date) if parsed is None: raise ValueError(f"Invalid base date: {base_date!r}") result = parsed + timedelta(days=offset_days) return result.isoformat()