Spaces:
Sleeping
Sleeping
| """ | |
| Natural language date/time parser for ClearSkies Analytics Agent | |
| Converts user queries like "yesterday", "Sept 25", "between 10:00 and 12:00 on Sept 24" into datetime objects | |
| NO EXTERNAL DEPENDENCIES - works with just standard library | |
| """ | |
| from datetime import datetime, timedelta | |
| from typing import Tuple, Optional | |
| import re | |
| class DateTimeParser: | |
| """Parse natural language date/time expressions""" | |
| def __init__(self, reference_date: Optional[datetime] = None): | |
| """ | |
| Initialize parser | |
| Args: | |
| reference_date: Reference date for relative expressions (default: now) | |
| """ | |
| self.reference_date = reference_date or datetime.now() | |
| # Month name mapping | |
| self.month_map = { | |
| 'jan': 1, 'january': 1, | |
| 'feb': 2, 'february': 2, | |
| 'mar': 3, 'march': 3, | |
| 'apr': 4, 'april': 4, | |
| 'may': 5, | |
| 'jun': 6, 'june': 6, | |
| 'jul': 7, 'july': 7, | |
| 'aug': 8, 'august': 8, | |
| 'sep': 9, 'sept': 9, 'september': 9, | |
| 'oct': 10, 'october': 10, | |
| 'nov': 11, 'november': 11, | |
| 'dec': 12, 'december': 12 | |
| } | |
| def parse(self, text: str) -> Tuple[Optional[datetime], Optional[datetime]]: | |
| """ | |
| Parse natural language date/time into start and end datetime | |
| Args: | |
| text: User input text containing date/time references | |
| Returns: | |
| Tuple of (start_datetime, end_datetime) | |
| """ | |
| text = text.lower().strip() | |
| # Priority 1: Time range on specific date (most specific) | |
| result = self._try_time_range_on_date(text) | |
| if result: | |
| return result | |
| # Priority 2: Specific date patterns | |
| result = self._try_specific_patterns(text) | |
| if result: | |
| return result | |
| # Default: return None if no date found | |
| return None, None | |
| def _try_time_range_on_date(self, text: str) -> Optional[Tuple[datetime, datetime]]: | |
| """ | |
| Handle patterns like: | |
| - "between 10:00 and 12:00 on Sept 24" | |
| - "from 10:00 to 12:00 on Sept 24" | |
| - "10:00 to 12:00 on Sept 24" | |
| """ | |
| # Pattern 1: "between HH:MM and HH:MM on DATE" | |
| pattern1 = r'between\s+(\d{1,2}):(\d{2})\s+and\s+(\d{1,2}):(\d{2})\s+on\s+(.+?)$' | |
| match = re.search(pattern1, text, re.IGNORECASE) | |
| if match: | |
| start_hour = int(match.group(1)) | |
| start_min = int(match.group(2)) | |
| end_hour = int(match.group(3)) | |
| end_min = int(match.group(4)) | |
| date_str = match.group(5).strip() | |
| # Parse the date part (just the date, ignore times in date string) | |
| base_date = self._parse_date_only(date_str) | |
| if base_date: | |
| start = datetime(base_date.year, base_date.month, base_date.day, start_hour, start_min, 0, 0) | |
| end = datetime(base_date.year, base_date.month, base_date.day, end_hour, end_min, 59, 999999) | |
| return start, end | |
| # Pattern 2: "from HH:MM to HH:MM on DATE" | |
| pattern2 = r'from\s+(\d{1,2}):(\d{2})\s+to\s+(\d{1,2}):(\d{2})\s+on\s+(.+?)$' | |
| match = re.search(pattern2, text, re.IGNORECASE) | |
| if match: | |
| start_hour = int(match.group(1)) | |
| start_min = int(match.group(2)) | |
| end_hour = int(match.group(3)) | |
| end_min = int(match.group(4)) | |
| date_str = match.group(5).strip() | |
| base_date = self._parse_date_only(date_str) | |
| if base_date: | |
| start = datetime(base_date.year, base_date.month, base_date.day, start_hour, start_min, 0, 0) | |
| end = datetime(base_date.year, base_date.month, base_date.day, end_hour, end_min, 59, 999999) | |
| return start, end | |
| # Pattern 3: "HH:MM to HH:MM on DATE" (without "from") | |
| pattern3 = r'(\d{1,2}):(\d{2})\s+to\s+(\d{1,2}):(\d{2})\s+on\s+(.+?)$' | |
| match = re.search(pattern3, text, re.IGNORECASE) | |
| if match: | |
| start_hour = int(match.group(1)) | |
| start_min = int(match.group(2)) | |
| end_hour = int(match.group(3)) | |
| end_min = int(match.group(4)) | |
| date_str = match.group(5).strip() | |
| base_date = self._parse_date_only(date_str) | |
| if base_date: | |
| start = datetime(base_date.year, base_date.month, base_date.day, start_hour, start_min, 0, 0) | |
| end = datetime(base_date.year, base_date.month, base_date.day, end_hour, end_min, 59, 999999) | |
| return start, end | |
| return None | |
| def _parse_date_only(self, date_str: str) -> Optional[datetime]: | |
| """Parse a date string like 'Sept 24' or '9/24' into a datetime (time will be 00:00:00)""" | |
| # Try month name + day | |
| month_day_pattern = r'(jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|sept|september|oct|october|nov|november|dec|december)[a-z]*\s+(\d{1,2})' | |
| match = re.search(month_day_pattern, date_str, re.IGNORECASE) | |
| if match: | |
| month_str = match.group(1).lower() | |
| month = self.month_map.get(month_str) | |
| day = int(match.group(2)) | |
| year = self.reference_date.year | |
| # If the date is in the future, assume it was last year | |
| try: | |
| test_date = datetime(year, month, day) | |
| if test_date > self.reference_date: | |
| year -= 1 | |
| return datetime(year, month, day, 0, 0, 0) | |
| except ValueError: | |
| return None | |
| # Try MM/DD or M/D | |
| md_pattern = r'\b(\d{1,2})[/-](\d{1,2})\b' | |
| match = re.search(md_pattern, date_str) | |
| if match: | |
| month = int(match.group(1)) | |
| day = int(match.group(2)) | |
| year = self.reference_date.year | |
| try: | |
| return datetime(year, month, day, 0, 0, 0) | |
| except ValueError: | |
| return None | |
| # Try YYYY-MM-DD | |
| ymd_pattern = r'(\d{4})[/-](\d{1,2})[/-](\d{1,2})' | |
| match = re.search(ymd_pattern, date_str) | |
| if match: | |
| year = int(match.group(1)) | |
| month = int(match.group(2)) | |
| day = int(match.group(3)) | |
| try: | |
| return datetime(year, month, day, 0, 0, 0) | |
| except ValueError: | |
| return None | |
| return None | |
| def _try_specific_patterns(self, text: str) -> Optional[Tuple[datetime, datetime]]: | |
| """Try to match specific common patterns""" | |
| # Month name + day (Sept 25, September 25th) | |
| month_day_pattern = r'(jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|sept|september|oct|october|nov|november|dec|december)[a-z]*\s+(\d{1,2})' | |
| match = re.search(month_day_pattern, text, re.IGNORECASE) | |
| if match: | |
| month_str = match.group(1).lower() | |
| month = self.month_map.get(month_str) | |
| day = int(match.group(2)) | |
| year = self.reference_date.year | |
| # If the date is in the future, assume it was last year | |
| try: | |
| test_date = datetime(year, month, day) | |
| if test_date > self.reference_date: | |
| year -= 1 | |
| start = datetime(year, month, day, 0, 0, 0) | |
| end = datetime(year, month, day, 23, 59, 59) | |
| return start, end | |
| except ValueError: | |
| pass | |
| # YYYY-MM-DD or YYYY/MM/DD | |
| ymd_pattern = r'(\d{4})[/-](\d{1,2})[/-](\d{1,2})' | |
| match = re.search(ymd_pattern, text) | |
| if match: | |
| try: | |
| year = int(match.group(1)) | |
| month = int(match.group(2)) | |
| day = int(match.group(3)) | |
| start = datetime(year, month, day, 0, 0, 0) | |
| end = datetime(year, month, day, 23, 59, 59) | |
| return start, end | |
| except ValueError: | |
| pass | |
| # MM/DD or M/D | |
| md_pattern = r'\b(\d{1,2})[/-](\d{1,2})\b' | |
| match = re.search(md_pattern, text) | |
| if match: | |
| try: | |
| month = int(match.group(1)) | |
| day = int(match.group(2)) | |
| year = self.reference_date.year | |
| start = datetime(year, month, day, 0, 0, 0) | |
| end = datetime(year, month, day, 23, 59, 59) | |
| return start, end | |
| except ValueError: | |
| pass | |
| # Today | |
| if 'today' in text: | |
| start = self.reference_date.replace(hour=0, minute=0, second=0, microsecond=0) | |
| end = self.reference_date.replace(hour=23, minute=59, second=59, microsecond=999999) | |
| return start, end | |
| # Yesterday | |
| if 'yesterday' in text: | |
| yesterday = self.reference_date - timedelta(days=1) | |
| start = yesterday.replace(hour=0, minute=0, second=0, microsecond=0) | |
| end = yesterday.replace(hour=23, minute=59, second=59, microsecond=999999) | |
| return start, end | |
| # Last N hours | |
| hours_match = re.search(r'last (\d+) hour', text) | |
| if hours_match: | |
| hours = int(hours_match.group(1)) | |
| end = self.reference_date | |
| start = end - timedelta(hours=hours) | |
| return start, end | |
| # Last N days | |
| days_match = re.search(r'last (\d+) day', text) | |
| if days_match: | |
| days = int(days_match.group(1)) | |
| end = self.reference_date | |
| start = end - timedelta(days=days) | |
| return start, end | |
| # This week | |
| if 'this week' in text: | |
| start = self.reference_date - timedelta(days=self.reference_date.weekday()) | |
| start = start.replace(hour=0, minute=0, second=0, microsecond=0) | |
| end = self.reference_date | |
| return start, end | |
| # Last week | |
| if 'last week' in text: | |
| end = self.reference_date - timedelta(days=self.reference_date.weekday()) | |
| start = end - timedelta(days=7) | |
| start = start.replace(hour=0, minute=0, second=0, microsecond=0) | |
| end = end.replace(hour=23, minute=59, second=59, microsecond=999999) | |
| return start, end | |
| # This month | |
| if 'this month' in text: | |
| start = self.reference_date.replace(day=1, hour=0, minute=0, second=0, microsecond=0) | |
| end = self.reference_date | |
| return start, end | |
| # Time range patterns: "from X to Y" | |
| range_pattern = r'from\s+(.+?)\s+to\s+(.+?)(?:\s|$)' | |
| match = re.search(range_pattern, text, re.IGNORECASE) | |
| if match: | |
| start_str = match.group(1).strip() | |
| end_str = match.group(2).strip() | |
| # Try to parse each part recursively | |
| start_result = self.parse(start_str) | |
| end_result = self.parse(end_str) | |
| if start_result[0] and end_result[1]: | |
| return start_result[0], end_result[1] | |
| return None | |
| def extract_time_window(self, text: str) -> Optional[Tuple[datetime, datetime]]: | |
| """ | |
| Extract time window from query, with helpful error messages | |
| Args: | |
| text: User query | |
| Returns: | |
| (start_time, end_time) or None if no time found | |
| """ | |
| start, end = self.parse(text) | |
| if start and end: | |
| return start, end | |
| return None | |
| # Test function | |
| if __name__ == "__main__": | |
| print("Testing DateTimeParser...") | |
| parser = DateTimeParser(reference_date=datetime(2025, 9, 26, 14, 30)) | |
| test_queries = [ | |
| "yesterday", | |
| "today", | |
| "last 3 hours", | |
| "last 5 days", | |
| "Sept 25", | |
| "September 25th", | |
| "between 10:00 and 12:00 on Sept 24", | |
| "from 10:00 to 12:00 on Sept 24", | |
| "10:00 to 12:00 on Sept 24", | |
| "from Sept 24 to Sept 25", | |
| "this week", | |
| "last week", | |
| "9/25", | |
| "2025-09-25", | |
| ] | |
| print("\nTest queries:") | |
| for query in test_queries: | |
| start, end = parser.parse(query) | |
| if start and end: | |
| print(f"✅ '{query}'") | |
| print(f" Start: {start}") | |
| print(f" End: {end}") | |
| else: | |
| print(f"❌ '{query}' - Could not parse") | |
| print() | |