utils.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. # -*- coding: utf-8 -*-
  2. """
  3. Datums-/Zeit-Utilities fuer das Calendar-Plugin.
  4. Parsing von deutschen Datums-/Zeitangaben und TTS-Formatierung.
  5. """
  6. import re
  7. from datetime import date, datetime, time, timedelta
  8. from plugins.calendar.models import CalendarEvent
  9. # =========================================================================
  10. # Zeit-Parsing
  11. # =========================================================================
  12. def parse_time_string(s: str) -> tuple[int, int] | None:
  13. """
  14. Parst eine deutsche Zeitangabe.
  15. Beispiele:
  16. "15 Uhr" -> (15, 0)
  17. "9:30" -> (9, 30)
  18. "15:00 Uhr" -> (15, 0)
  19. "halb drei" -> (14, 30)
  20. Args:
  21. s: Zeitangabe als String
  22. Returns:
  23. (stunde, minute) oder None bei Fehler
  24. """
  25. if not s:
  26. return None
  27. s = s.strip().lower()
  28. # "15:00 Uhr" oder "15:00" oder "9:30"
  29. m = re.match(r"(\d{1,2}):(\d{2})\s*(?:uhr)?$", s)
  30. if m:
  31. h, mi = int(m.group(1)), int(m.group(2))
  32. if 0 <= h <= 23 and 0 <= mi <= 59:
  33. return (h, mi)
  34. # "15 Uhr" oder "15uhr"
  35. m = re.match(r"(\d{1,2})\s*uhr$", s)
  36. if m:
  37. h = int(m.group(1))
  38. if 0 <= h <= 23:
  39. return (h, 0)
  40. # "15 Uhr 30"
  41. m = re.match(r"(\d{1,2})\s*uhr\s*(\d{1,2})$", s)
  42. if m:
  43. h, mi = int(m.group(1)), int(m.group(2))
  44. if 0 <= h <= 23 and 0 <= mi <= 59:
  45. return (h, mi)
  46. # Nur Zahl (als Stunde interpretieren)
  47. m = re.match(r"(\d{1,2})$", s)
  48. if m:
  49. h = int(m.group(1))
  50. if 0 <= h <= 23:
  51. return (h, 0)
  52. return None
  53. # =========================================================================
  54. # Datum-Parsing
  55. # =========================================================================
  56. _WEEKDAY_MAP = {
  57. "montag": 0,
  58. "dienstag": 1,
  59. "mittwoch": 2,
  60. "donnerstag": 3,
  61. "freitag": 4,
  62. "samstag": 5,
  63. "sonntag": 6,
  64. }
  65. def parse_date_string(s: str) -> date | None:
  66. """
  67. Parst eine deutsche Datumsangabe.
  68. Beispiele:
  69. "heute" -> today
  70. "morgen" -> tomorrow
  71. "uebermorgen" -> day after tomorrow
  72. "Montag" -> naechster Montag
  73. "15.03." -> 15. Maerz (aktuelles Jahr)
  74. "15.03.2026" -> 15. Maerz 2026
  75. Args:
  76. s: Datumsangabe als String
  77. Returns:
  78. date oder None bei Fehler
  79. """
  80. if not s:
  81. return None
  82. s = s.strip().lower()
  83. today = date.today()
  84. # Relative Angaben
  85. if s in ("heute", "today"):
  86. return today
  87. if s in ("morgen", "tomorrow"):
  88. return today + timedelta(days=1)
  89. if s in ("uebermorgen", "übermorgen"):
  90. return today + timedelta(days=2)
  91. # Wochentag
  92. for day_name, day_idx in _WEEKDAY_MAP.items():
  93. if s == day_name:
  94. current_idx = today.weekday()
  95. diff = day_idx - current_idx
  96. if diff <= 0:
  97. diff += 7
  98. return today + timedelta(days=diff)
  99. # "15.03." oder "15.3."
  100. m = re.match(r"(\d{1,2})\.(\d{1,2})\.$", s)
  101. if m:
  102. day, month = int(m.group(1)), int(m.group(2))
  103. try:
  104. target = date(today.year, month, day)
  105. # Falls in Vergangenheit, naechstes Jahr
  106. if target < today:
  107. target = date(today.year + 1, month, day)
  108. return target
  109. except ValueError:
  110. return None
  111. # "15.03.2026"
  112. m = re.match(r"(\d{1,2})\.(\d{1,2})\.(\d{4})$", s)
  113. if m:
  114. day, month, year = int(m.group(1)), int(m.group(2)), int(m.group(3))
  115. try:
  116. return date(year, month, day)
  117. except ValueError:
  118. return None
  119. return None
  120. # =========================================================================
  121. # TTS-Formatierung
  122. # =========================================================================
  123. _MONTH_NAMES = [
  124. "", "Januar", "Februar", "Maerz", "April", "Mai", "Juni",
  125. "Juli", "August", "September", "Oktober", "November", "Dezember",
  126. ]
  127. _WEEKDAY_NAMES = [
  128. "Montag", "Dienstag", "Mittwoch", "Donnerstag",
  129. "Freitag", "Samstag", "Sonntag",
  130. ]
  131. def format_event_time(event: CalendarEvent) -> str:
  132. """
  133. Formatiert die Uhrzeit eines Events fuer TTS.
  134. Beispiele:
  135. "um 15 Uhr"
  136. "von 15 bis 16 Uhr"
  137. "ganztaegig"
  138. Args:
  139. event: Kalender-Event
  140. Returns:
  141. Formatierter Zeitstring
  142. """
  143. if event.all_day:
  144. return "ganztaegig"
  145. start_h = event.start.hour
  146. start_m = event.start.minute
  147. end_h = event.end.hour
  148. end_m = event.end.minute
  149. start_str = _format_clock_time(start_h, start_m)
  150. end_str = _format_clock_time(end_h, end_m)
  151. if start_str == end_str:
  152. return f"um {start_str}"
  153. return f"von {start_str} bis {end_str}"
  154. def _format_clock_time(h: int, m: int) -> str:
  155. """Formatiert Stunde:Minute als TTS-String."""
  156. if m == 0:
  157. return f"{h} Uhr"
  158. return f"{h} Uhr {m:02d}"
  159. def format_event_date(event: CalendarEvent) -> str:
  160. """
  161. Formatiert das Datum eines Events fuer TTS.
  162. Beispiele:
  163. "heute"
  164. "morgen"
  165. "am Freitag"
  166. "am 15. Maerz"
  167. Args:
  168. event: Kalender-Event
  169. Returns:
  170. Formatierter Datumsstring
  171. """
  172. event_date = event.start.date()
  173. today = date.today()
  174. diff = (event_date - today).days
  175. if diff == 0:
  176. return "heute"
  177. if diff == 1:
  178. return "morgen"
  179. if diff == 2:
  180. return "uebermorgen"
  181. if 0 < diff <= 6:
  182. return f"am {_WEEKDAY_NAMES[event_date.weekday()]}"
  183. day = event_date.day
  184. month = _MONTH_NAMES[event_date.month]
  185. return f"am {day}. {month}"
  186. def format_event_for_tts(event: CalendarEvent) -> str:
  187. """
  188. Formatiert einen kompletten Termin fuer TTS.
  189. Beispiel: "Stand-Up heute von 14 bis 15 Uhr"
  190. Args:
  191. event: Kalender-Event
  192. Returns:
  193. Kompletter TTS-String
  194. """
  195. date_str = format_event_date(event)
  196. time_str = format_event_time(event)
  197. if event.all_day:
  198. return f"{event.title} {date_str}, {time_str}"
  199. return f"{event.title} {date_str} {time_str}"
  200. def format_attendees_for_tts(attendees: list[str]) -> str:
  201. """
  202. Formatiert eine Teilnehmerliste fuer TTS.
  203. Beispiel: "André, Max und Lisa"
  204. Args:
  205. attendees: Liste von Teilnehmernamen
  206. Returns:
  207. Formatierter String
  208. """
  209. if not attendees:
  210. return ""
  211. if len(attendees) == 1:
  212. return attendees[0]
  213. return ", ".join(attendees[:-1]) + " und " + attendees[-1]
  214. def get_day_range(target_date: date) -> tuple[datetime, datetime]:
  215. """
  216. Gibt den Zeitbereich eines Tages zurueck.
  217. Args:
  218. target_date: Zieldatum
  219. Returns:
  220. (Tagesanfang 00:00, Tagesende 23:59:59)
  221. """
  222. start = datetime.combine(target_date, time.min)
  223. end = datetime.combine(target_date, time.max)
  224. return (start, end)