File size: 3,144 Bytes
3c5146b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
"""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()