File size: 4,308 Bytes
a55ea57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import calendar
from datetime import datetime, timedelta, timezone
from typing import Optional
import re

_DATE_INPUT_FORMATS = (
    "%d/%m/%Y",
    "%Y-%m-%d",
    "%Y/%m/%d",
    "%d-%m-%Y",
    "%B %d, %Y",
    "%b %d, %Y",
    "%B %d %Y",
    "%b %d %Y",
    "%d %B %Y",
    "%d %b %Y",
    "%B-%d-%Y",
    "%b-%d-%Y",
    "%Y-%m-%dT%H:%M:%S",
    "%Y-%m-%dT%H:%M:%S%z",
    "%Y-%m-%dT%H:%M:%S.%f",
    "%Y-%m-%dT%H:%M:%S.%f%z",
    "%Y-%m-%d %H:%M:%S",
    "%Y-%m-%d %H:%M:%S%z",
    "%Y-%m-%d %H:%M:%S.%f",
    "%Y-%m-%d %H:%M:%S.%f%z",
    "%I:%M %p on %d %B, %Y",
    "%I:%M %p on %d %b, %Y",
)

_RELATIVE_PATTERNS = (
    (re.compile(r"^yesterday$", re.I), lambda ref: ref - timedelta(days=1)),
    (re.compile(r"^tomorrow$", re.I), lambda ref: ref + timedelta(days=1)),
    (re.compile(r"^today$", re.I), lambda ref: ref),
    (re.compile(r"^last\s+week$", re.I), lambda ref: ref - timedelta(days=7)),
    (re.compile(r"^next\s+week$", re.I), lambda ref: ref + timedelta(days=7)),
    (re.compile(r"^last\s+month$", re.I), lambda ref: _shift_months(ref, -1)),
    (re.compile(r"^next\s+month$", re.I), lambda ref: _shift_months(ref, 1)),
    (
        re.compile(r"^(\d+)\s+days?\s+ago$", re.I),
        lambda ref, m: ref - timedelta(days=int(m.group(1))),
    ),
    (
        re.compile(r"^in\s+(\d+)\s+days?$", re.I),
        lambda ref, m: ref + timedelta(days=int(m.group(1))),
    ),
    (
        re.compile(r"^(\d+)\s+weeks?\s+ago$", re.I),
        lambda ref, m: ref - timedelta(weeks=int(m.group(1))),
    ),
)

_WEEKDAY_NAMES = {
    "monday": 0,
    "tuesday": 1,
    "wednesday": 2,
    "thursday": 3,
    "friday": 4,
    "saturday": 5,
    "sunday": 6,
}


def _shift_months(ref: datetime, delta: int) -> datetime:
    month = ref.month - 1 + delta
    year = ref.year + month // 12
    month = month % 12 + 1
    day = min(ref.day, [31, 29 if calendar.isleap(year) else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1])
    return ref.replace(year=year, month=month, day=day)


def to_naive_utc(value: datetime) -> datetime:
    if value.tzinfo is None:
        return value
    return value.astimezone(timezone.utc).replace(tzinfo=None)


def parse_date_string(value: Optional[str]) -> Optional[datetime]:
    if not value or not isinstance(value, str):
        return None
    cleaned = value.strip()
    for fmt in _DATE_INPUT_FORMATS:
        try:
            parsed = datetime.strptime(cleaned, fmt)
        except ValueError:
            continue
        return to_naive_utc(parsed)
    return None


def normalize_date_string(value: Optional[str]) -> Optional[str]:
    if not value or not isinstance(value, str):
        return value
    parsed = parse_date_string(value)
    if parsed is not None:
        return parsed.strftime("%d/%m/%Y")
    return value.strip()


def resolve_relative_date(
    value: Optional[str],
    reference_time: Optional[str | datetime] = None,
) -> Optional[str]:
    if not value or not isinstance(value, str):
        return value
    cleaned = value.strip()
    absolute = normalize_date_string(cleaned)
    if absolute != cleaned or parse_date_string(cleaned) is not None:
        return absolute

    if reference_time is None:
        return cleaned
    if isinstance(reference_time, datetime):
        ref = reference_time
    else:
        ref = parse_date_string(str(reference_time))
        if ref is None:
            return cleaned

    for pattern, resolver in _RELATIVE_PATTERNS:
        match = pattern.match(cleaned)
        if not match:
            continue
        if match.lastindex:
            resolved = resolver(ref, match)
        else:
            resolved = resolver(ref)
        return resolved.strftime("%d/%m/%Y")

    weekday_match = re.match(r"^(last|this|next)\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)$", cleaned, re.I)
    if weekday_match:
        mode = weekday_match.group(1).lower()
        target = _WEEKDAY_NAMES[weekday_match.group(2).lower()]
        current = ref.weekday()
        if mode == "this":
            delta = (target - current) % 7
        elif mode == "next":
            delta = (target - current) % 7 or 7
        else:
            delta = -((current - target) % 7 or 7)
        return (ref + timedelta(days=delta)).strftime("%d/%m/%Y")

    return cleaned