delphi-pump-analytics / core /date_parser.py
pandeydigant31's picture
Initial deploy: DELPHI Pump Analytics (renamed from MURPHY)
199bfa3 verified
Raw
History Blame Contribute Delete
12.8 kB
"""
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()