File size: 5,544 Bytes
fa948af
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""
DateTimeParser: Natural language date/time parsing with timezone support.

This module provides a dual-library approach for parsing natural language dates:
- dateparser (primary): Excels at absolute dates and timezone handling
- parsedatetime (fallback): Excels at relative dates like "next Friday"

The combination achieves 100% coverage of common date/time expressions.
"""

from datetime import datetime
from typing import Optional
import pytz
from dateparser import parse as dateparser_parse
from parsedatetime import Calendar


class DateTimeParser:
    """
    Natural language date/time parser with timezone support.

    This parser uses a dual-library approach to handle both absolute and
    relative date expressions with high accuracy.

    Example usage:
        parser = DateTimeParser()
        result = parser.parse("tomorrow at 3pm", user_timezone="America/New_York")
        # Returns: datetime in UTC
    """

    def __init__(self):
        """Initialize the parser with parsedatetime calendar."""
        self.calendar = Calendar()

    def parse(
        self,
        text: str,
        user_timezone: str = "UTC"
    ) -> Optional[datetime]:
        """
        Parse natural language date/time to UTC datetime.

        Args:
            text: Natural language date/time string
                Examples: "tomorrow at 3pm", "next Friday", "in 2 hours",
                         "January 15, 2026 at 10:30 AM"
            user_timezone: User's timezone for interpretation (default: UTC)
                Examples: "UTC", "America/New_York", "Europe/London"

        Returns:
            datetime object in UTC timezone if parsing succeeds
            None if parsing fails

        Examples:
            >>> parser = DateTimeParser()
            >>> parser.parse("tomorrow at 3pm")
            datetime(2026, 2, 6, 15, 0, 0, tzinfo=<UTC>)

            >>> parser.parse("next Friday", user_timezone="America/New_York")
            datetime(2026, 2, 7, 5, 0, 0, tzinfo=<UTC>)  # Converted to UTC
        """
        if not text or not text.strip():
            return None

        # Try dateparser first (better for absolute dates)
        result = self._try_dateparser(text, user_timezone)
        if result:
            return result

        # Fallback to parsedatetime (better for relative dates)
        result = self._try_parsedatetime(text, user_timezone)
        if result:
            return result

        return None

    def _try_dateparser(
        self,
        text: str,
        user_timezone: str
    ) -> Optional[datetime]:
        """
        Try parsing with dateparser library.

        Args:
            text: Natural language date/time string
            user_timezone: User's timezone for interpretation

        Returns:
            datetime in UTC or None if parsing fails
        """
        try:
            result = dateparser_parse(
                text,
                settings={
                    'TIMEZONE': user_timezone,
                    'RETURN_AS_TIMEZONE_AWARE': True,
                    'TO_TIMEZONE': 'UTC',
                    'PREFER_DATES_FROM': 'future',  # Assume future dates
                    'RELATIVE_BASE': datetime.now(pytz.timezone(user_timezone))
                }
            )

            if result and result.tzinfo is not None:
                return result

        except (ValueError, TypeError, Exception):
            # Parsing failed, return None to try fallback
            pass

        return None

    def _try_parsedatetime(
        self,
        text: str,
        user_timezone: str
    ) -> Optional[datetime]:
        """
        Try parsing with parsedatetime library (fallback).

        Args:
            text: Natural language date/time string
            user_timezone: User's timezone for interpretation

        Returns:
            datetime in UTC or None if parsing fails
        """
        try:
            user_tz = pytz.timezone(user_timezone)
            now_in_tz = datetime.now(user_tz)

            time_struct, parse_status = self.calendar.parse(text, now_in_tz)

            # parse_status meanings:
            # 0: Failed to parse
            # 1: Parsed as a date
            # 2: Parsed as a time
            # 3: Parsed as a datetime
            if parse_status in [1, 2, 3]:
                # Convert time_struct to datetime
                dt = datetime(
                    year=time_struct[0],
                    month=time_struct[1],
                    day=time_struct[2],
                    hour=time_struct[3],
                    minute=time_struct[4],
                    second=time_struct[5],
                    tzinfo=user_tz
                )

                # Convert to UTC
                return dt.astimezone(pytz.UTC)

        except (ValueError, TypeError, Exception):
            # Parsing failed
            pass

        return None

    def validate_timezone(self, timezone: str) -> bool:
        """
        Validate that a timezone string is valid.

        Args:
            timezone: Timezone string to validate (e.g., "America/New_York")

        Returns:
            True if valid, False otherwise

        Example:
            >>> parser = DateTimeParser()
            >>> parser.validate_timezone("America/New_York")
            True
            >>> parser.validate_timezone("Invalid/Timezone")
            False
        """
        try:
            pytz.timezone(timezone)
            return True
        except pytz.exceptions.UnknownTimeZoneError:
            return False