# -*- coding: utf-8 -*- """ Einfacher deutscher Zeitparser fuer Erinnerungen. Parst natuerlichsprachliche Zeitangaben wie: - "morgen", "uebermorgen" - "morgen um 14 Uhr" - "in 2 Stunden", "in 30 Minuten" - "naechsten Montag" - "heute Abend", "heute Nachmittag" - "2 Uhr" (ambiguous: VM/NM Rueckfrage) - "14 Uhr" (eindeutig) """ import re from dataclasses import dataclass from datetime import date, datetime, timedelta @dataclass class ParsedTime: """Ergebnis des Zeitparsers.""" dt: datetime | None = None ambiguous: bool = False option_a: str = "" # z.B. "2 Uhr morgens" option_b: str = "" # z.B. "14 Uhr nachmittags" error: str = "" def parse_german_time(text: str, default_time: str = "09:00") -> ParsedTime: """ Parst einen deutschen Zeittext in ein datetime. Args: text: Natuerlichsprachliche Zeitangabe default_time: Standard-Uhrzeit wenn keine angegeben (HH:MM) Returns: ParsedTime mit geparster Zeit oder Fehler """ text_lower = text.lower().strip() now = datetime.now() default_hour, default_minute = _parse_hm(default_time) # "in X Stunden/Minuten" — relative Zeit relative = _parse_relative(text_lower, now) if relative: return relative # Tag bestimmen target_day = _parse_day(text_lower, now) # Uhrzeit bestimmen time_result = _parse_time_of_day(text_lower, now) if target_day is None and time_result is None: return ParsedTime(error=f"Zeitangabe '{text}' nicht erkannt") if target_day is None: # Nur Uhrzeit angegeben → heute oder morgen target_day = now.date() if time_result and not time_result.ambiguous and time_result.dt: # Wenn die Zeit schon vorbei ist → morgen candidate = datetime.combine(target_day, time_result.dt.time()) if candidate <= now: target_day = target_day + timedelta(days=1) if time_result is None: # Nur Tag → Default-Uhrzeit dt = datetime.combine(target_day, datetime.min.time().replace( hour=default_hour, minute=default_minute, )) return ParsedTime(dt=dt) if time_result.ambiguous: # Tag + ambige Uhrzeit time_result.option_a = f"{time_result.option_a}" time_result.option_b = f"{time_result.option_b}" return time_result if time_result.dt: dt = datetime.combine(target_day, time_result.dt.time()) return ParsedTime(dt=dt) return ParsedTime(error=f"Zeitangabe '{text}' nicht erkannt") def _parse_hm(time_str: str) -> tuple[int, int]: """Parst HH:MM String.""" parts = time_str.split(":") return int(parts[0]), int(parts[1]) if len(parts) > 1 else 0 def _parse_relative(text: str, now: datetime) -> ParsedTime | None: """Parst relative Zeitangaben wie 'in 2 Stunden'.""" # "in X Stunden" match = re.search(r"in\s+(\d+)\s+stunde[n]?", text) if match: hours = int(match.group(1)) return ParsedTime(dt=now + timedelta(hours=hours)) # "in einer Stunde" if re.search(r"in\s+einer?\s+stunde", text): return ParsedTime(dt=now + timedelta(hours=1)) # "in X Minuten" match = re.search(r"in\s+(\d+)\s+minute[n]?", text) if match: minutes = int(match.group(1)) return ParsedTime(dt=now + timedelta(minutes=minutes)) # "in einer halben Stunde" if re.search(r"in\s+einer?\s+halben\s+stunde", text): return ParsedTime(dt=now + timedelta(minutes=30)) return None def _parse_day(text: str, now: datetime) -> date | None: """Parst den Tag aus dem Text.""" today = now.date() if "heute" in text: return today if "morgen" in text and "uebermorgen" not in text and "übermorgen" not in text: return today + timedelta(days=1) if "uebermorgen" in text or "übermorgen" in text: return today + timedelta(days=2) # "naechsten/nächsten Montag" etc. wochentage = { "montag": 0, "dienstag": 1, "mittwoch": 2, "donnerstag": 3, "freitag": 4, "samstag": 5, "sonntag": 6, } for tag, weekday in wochentage.items(): if tag in text: days_ahead = (weekday - today.weekday()) % 7 if days_ahead == 0: days_ahead = 7 # Naechste Woche if "naechsten" in text or "nächsten" in text or "kommenden" in text: if days_ahead <= 0: days_ahead += 7 return today + timedelta(days=days_ahead) return None def _parse_time_of_day(text: str, now: datetime) -> ParsedTime | None: """Parst die Uhrzeit aus dem Text.""" # "heute Abend", "heute Nachmittag", "heute Morgen" if "abend" in text or "abends" in text: return ParsedTime(dt=datetime.combine(now.date(), datetime.min.time().replace(hour=19))) if "nachmittag" in text or "nachmittags" in text: return ParsedTime(dt=datetime.combine(now.date(), datetime.min.time().replace(hour=15))) if re.search(r"\bmorgens?\b", text) and "morgen" not in text.replace("morgens", ""): return ParsedTime(dt=datetime.combine(now.date(), datetime.min.time().replace(hour=8))) if "mittag" in text and "nachmittag" not in text: return ParsedTime(dt=datetime.combine(now.date(), datetime.min.time().replace(hour=12))) # "um HH:MM [Uhr]" match = re.search(r"(?:um\s+)?(\d{1,2})[:\.](\d{2})\s*(?:uhr)?", text) if match: hour = int(match.group(1)) minute = int(match.group(2)) if 0 <= hour <= 23 and 0 <= minute <= 59: return ParsedTime(dt=datetime.combine(now.date(), datetime.min.time().replace( hour=hour, minute=minute, ))) # "um X Uhr" oder "X Uhr" match = re.search(r"(?:um\s+)?(\d{1,2})\s*uhr", text) if match: hour = int(match.group(1)) if 13 <= hour <= 23: # Eindeutig Nachmittag/Abend return ParsedTime(dt=datetime.combine(now.date(), datetime.min.time().replace(hour=hour))) if 0 <= hour <= 12: if hour <= 6: # 1-6 Uhr: wahrscheinlich Nachmittag → trotzdem Rueckfrage return ParsedTime( ambiguous=True, option_a=f"{hour} Uhr morgens", option_b=f"{hour + 12} Uhr nachmittags", ) if hour >= 7 and hour <= 12: # 7-12 Uhr: wahrscheinlich Vormittag, aber Rueckfrage bei unklarem Kontext # Wenn "morgens" oder "vormittag" im Text → eindeutig if "morgens" in text or "vormittag" in text or "frueh" in text or "früh" in text: return ParsedTime(dt=datetime.combine(now.date(), datetime.min.time().replace(hour=hour))) if "nachmittag" in text or "abend" in text: return ParsedTime(dt=datetime.combine(now.date(), datetime.min.time().replace( hour=hour + 12 if hour != 12 else 12, ))) # 7-11 Uhr ohne Kontext → vormittag (hohe Wahrscheinlichkeit) if 7 <= hour <= 11: return ParsedTime(dt=datetime.combine(now.date(), datetime.min.time().replace(hour=hour))) # 12 Uhr → eindeutig Mittag if hour == 12: return ParsedTime(dt=datetime.combine(now.date(), datetime.min.time().replace(hour=12))) return None