logic_eval.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. # -*- coding: utf-8 -*-
  2. """
  3. Logik-Ausdruck-Evaluator fuer Ternary-Ausdruecke.
  4. Wertet Ausdruecke der Form (condition ? result_true : result_false) aus.
  5. Unterstuetzt Vergleichsoperatoren, Typ-Pruefung, Else-If-Ketten (::),
  6. und Boolean-Keywords.
  7. Portiert von PrrintText_cpp/format.cpp (logic Namespace).
  8. """
  9. from __future__ import annotations
  10. import re
  11. from typing import Any
  12. # Boolean-Keywords
  13. _TRUE_KEYWORDS = {"true", "on", "yes", "ok", "success", "1", "wahr", "ja", "an"}
  14. _FALSE_KEYWORDS = {"false", "off", "no", "error", "failed", "0", "falsch", "nein", "aus"}
  15. # Vergleichsoperatoren (laengste zuerst, damit >= vor > matcht)
  16. _OPERATORS = ["===", "!==", ">=", "<=", "!=", "==", ">", "<"]
  17. def _try_parse_number(s: str) -> float | None:
  18. """Versucht einen String als Zahl zu parsen."""
  19. s = s.strip()
  20. if not s:
  21. return None
  22. try:
  23. return float(s)
  24. except ValueError:
  25. # Lokalisierte Formate: 1.234,56 → 1234.56
  26. if "," in s:
  27. try:
  28. return float(s.replace(".", "").replace(",", "."))
  29. except ValueError:
  30. pass
  31. return None
  32. def _try_parse_version(s: str) -> tuple[int, ...] | None:
  33. """Versucht einen String als Version (x.y.z.w) zu parsen."""
  34. s = s.strip()
  35. parts = s.split(".")
  36. if len(parts) < 2 or len(parts) > 4:
  37. return None
  38. try:
  39. nums = tuple(int(p) for p in parts)
  40. return nums
  41. except ValueError:
  42. return None
  43. def _is_truthy(value: str) -> bool:
  44. """Prueft ob ein Wert als 'wahr' gilt."""
  45. v = value.strip().lower()
  46. if v in _TRUE_KEYWORDS:
  47. return True
  48. if v in _FALSE_KEYWORDS:
  49. return False
  50. num = _try_parse_number(v)
  51. if num is not None:
  52. return num > 0
  53. # Nicht-leerer String → True
  54. return bool(v)
  55. def _compare(lhs: str, op: str, rhs: str) -> bool:
  56. """Vergleicht zwei Werte mit dem gegebenen Operator."""
  57. lhs = lhs.strip()
  58. rhs = rhs.strip()
  59. # Typ-Pruefung mit ===
  60. if op == "===":
  61. return _type_check(lhs, rhs)
  62. if op == "!==":
  63. return not _type_check(lhs, rhs)
  64. # Zahlen vergleichen
  65. lnum = _try_parse_number(lhs)
  66. rnum = _try_parse_number(rhs)
  67. if lnum is not None and rnum is not None:
  68. return _compare_numbers(lnum, op, rnum)
  69. # Versionen vergleichen
  70. lver = _try_parse_version(lhs)
  71. rver = _try_parse_version(rhs)
  72. if lver is not None and rver is not None:
  73. return _compare_versions(lver, op, rver)
  74. # Boolean-Keywords
  75. lb = lhs.lower() in _TRUE_KEYWORDS
  76. rb = rhs.lower() in _TRUE_KEYWORDS
  77. if lhs.lower() in (_TRUE_KEYWORDS | _FALSE_KEYWORDS) and \
  78. rhs.lower() in (_TRUE_KEYWORDS | _FALSE_KEYWORDS):
  79. return _compare_numbers(float(lb), op, float(rb))
  80. # String-Vergleich (case-insensitive)
  81. llow = lhs.lower()
  82. rlow = rhs.lower()
  83. if op == "==":
  84. return llow == rlow
  85. if op == "!=":
  86. return llow != rlow
  87. if op == "<":
  88. return llow < rlow
  89. if op == ">":
  90. return llow > rlow
  91. if op == "<=":
  92. return llow <= rlow
  93. if op == ">=":
  94. return llow >= rlow
  95. return False
  96. def _compare_numbers(a: float, op: str, b: float) -> bool:
  97. if op == "==":
  98. return a == b
  99. if op == "!=":
  100. return a != b
  101. if op == "<":
  102. return a < b
  103. if op == ">":
  104. return a > b
  105. if op == "<=":
  106. return a <= b
  107. if op == ">=":
  108. return a >= b
  109. return False
  110. def _compare_versions(a: tuple[int, ...], op: str, b: tuple[int, ...]) -> bool:
  111. # Auf gleiche Laenge auffuellen
  112. max_len = max(len(a), len(b))
  113. a_padded = a + (0,) * (max_len - len(a))
  114. b_padded = b + (0,) * (max_len - len(b))
  115. if op == "==":
  116. return a_padded == b_padded
  117. if op == "!=":
  118. return a_padded != b_padded
  119. if op == "<":
  120. return a_padded < b_padded
  121. if op == ">":
  122. return a_padded > b_padded
  123. if op == "<=":
  124. return a_padded <= b_padded
  125. if op == ">=":
  126. return a_padded >= b_padded
  127. return False
  128. def _type_check(value: str, type_name: str) -> bool:
  129. """Typ-Pruefung: value === int/float/bool/byte."""
  130. t = type_name.lower().strip()
  131. v = value.strip()
  132. if t == "int":
  133. try:
  134. int(v)
  135. return "." not in v
  136. except ValueError:
  137. return False
  138. if t in ("float", "decimal", "number", "numeric"):
  139. return _try_parse_number(v) is not None
  140. if t in ("bool", "boolean"):
  141. return v.lower() in (_TRUE_KEYWORDS | _FALSE_KEYWORDS)
  142. if t == "byte":
  143. try:
  144. n = int(v)
  145. return 0 <= n <= 255
  146. except ValueError:
  147. return False
  148. if t in ("string", "str"):
  149. return True
  150. if t == "empty":
  151. return v == ""
  152. return False
  153. def _find_at_depth_0(text: str, needle: str, start: int = 0) -> int:
  154. """Findet needle bei Klammer-Tiefe 0."""
  155. depth = 0
  156. i = start
  157. nlen = len(needle)
  158. while i < len(text):
  159. if text[i] == "(":
  160. depth += 1
  161. elif text[i] == ")":
  162. depth -= 1
  163. elif depth == 0 and text[i:i + nlen] == needle:
  164. return i
  165. i += 1
  166. return -1
  167. def _split_branches(text: str) -> list[str]:
  168. """Teilt den Ausdruck an :: bei Tiefe 0."""
  169. branches: list[str] = []
  170. start = 0
  171. while True:
  172. pos = _find_at_depth_0(text, "::", start)
  173. if pos == -1:
  174. branches.append(text[start:])
  175. break
  176. branches.append(text[start:pos])
  177. start = pos + 2
  178. return branches
  179. def _parse_condition(text: str) -> tuple[str, str, str] | None:
  180. """Parst 'lhs op rhs' aus einem Bedingungsstring."""
  181. text = text.strip()
  182. # Operator finden (laengste zuerst)
  183. for op in _OPERATORS:
  184. pos = text.find(op)
  185. if pos != -1:
  186. lhs = text[:pos].strip()
  187. rhs = text[pos + len(op):].strip()
  188. return lhs, op, rhs
  189. return None
  190. def eval_logic(expr: str) -> str | None:
  191. """
  192. Wertet einen logischen Ternary-Ausdruck aus.
  193. Format: condition ? result_true : result_false
  194. Else-If: c1 ? r1 :: c2 ? r2 :: ... : default
  195. Args:
  196. expr: Der Ausdruck (ohne die aeusseren Klammern).
  197. Returns:
  198. Das Ergebnis als String oder None bei Parse-Fehler.
  199. """
  200. expr = expr.strip()
  201. if not expr:
  202. return None
  203. # Branches aufteilen (an :: bei Tiefe 0)
  204. branches = _split_branches(expr)
  205. for i, branch in enumerate(branches):
  206. branch = branch.strip()
  207. is_last = (i == len(branches) - 1)
  208. # ? finden bei Tiefe 0
  209. q_pos = _find_at_depth_0(branch, "?")
  210. if q_pos == -1:
  211. if is_last:
  212. # Letzter Branch ohne ? → Default-Wert
  213. return branch
  214. continue
  215. condition_str = branch[:q_pos].strip()
  216. rest = branch[q_pos + 1:]
  217. # Bei letztem Branch: true_result : false_result
  218. # Bei nicht-letztem Branch: nur true_result
  219. if is_last:
  220. colon_pos = _find_at_depth_0(rest, ":")
  221. if colon_pos == -1:
  222. true_result = rest.strip()
  223. false_result = ""
  224. else:
  225. true_result = rest[:colon_pos].strip()
  226. false_result = rest[colon_pos + 1:].strip()
  227. else:
  228. true_result = rest.strip()
  229. false_result = None # Kein False-Zweig bei Nicht-letztem Branch
  230. # Bedingung auswerten
  231. cond_result = _eval_condition(condition_str)
  232. if cond_result:
  233. return true_result
  234. elif is_last and false_result is not None:
  235. return false_result
  236. return None
  237. def _eval_condition(condition: str) -> bool:
  238. """Wertet eine einzelne Bedingung aus."""
  239. condition = condition.strip()
  240. # Versuche Operator zu finden
  241. parsed = _parse_condition(condition)
  242. if parsed:
  243. lhs, op, rhs = parsed
  244. return _compare(lhs, op, rhs)
  245. # Kein Operator → Truthiness-Pruefung
  246. return _is_truthy(condition)