| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- # -*- 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
|