math_eval.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. # -*- coding: utf-8 -*-
  2. """
  3. Mathematischer Ausdruck-Evaluator (Recursive Descent Parser).
  4. Wertet Ausdruecke der Form (:expression) aus.
  5. Unterstuetzt: +, -, *, /, %, ^ (Potenz), &, |, xor, ~, not, and, or, nand, nor.
  6. Klammern fuer Gruppierung. Hex-Literale (0xFF). Float/Int.
  7. Praezedenz (niedrig → hoch):
  8. nor → nand → | / or → xor → & / and → + - → * / % → ^ → unaer (- ~ not)
  9. Portiert von PrrintText_cpp/format.cpp (matheval Namespace).
  10. """
  11. from __future__ import annotations
  12. import math
  13. from enum import Enum, auto
  14. from typing import Any
  15. class _TokenType(Enum):
  16. NUMBER = auto()
  17. PLUS = auto()
  18. MINUS = auto()
  19. STAR = auto()
  20. SLASH = auto()
  21. PERCENT = auto()
  22. CARET = auto() # ^
  23. AMP = auto() # &
  24. PIPE = auto() # |
  25. TILDE = auto() # ~
  26. LPAREN = auto()
  27. RPAREN = auto()
  28. KW_AND = auto()
  29. KW_OR = auto()
  30. KW_XOR = auto()
  31. KW_NOT = auto()
  32. KW_NAND = auto()
  33. KW_NOR = auto()
  34. END = auto()
  35. ERROR = auto()
  36. class _Token:
  37. __slots__ = ("type", "value")
  38. def __init__(self, tt: _TokenType, value: float = 0.0) -> None:
  39. self.type = tt
  40. self.value = value
  41. def _tokenize(expr: str) -> list[_Token]:
  42. """Tokenisiert einen mathematischen Ausdruck."""
  43. tokens: list[_Token] = []
  44. i = 0
  45. n = len(expr)
  46. while i < n:
  47. ch = expr[i]
  48. # Whitespace ueberspringen
  49. if ch in " \t\r\n":
  50. i += 1
  51. continue
  52. # Zahlen (int, float, hex, scientific)
  53. if ch.isdigit() or (ch == "." and i + 1 < n and expr[i + 1].isdigit()):
  54. start = i
  55. # Hex: 0x...
  56. if ch == "0" and i + 1 < n and expr[i + 1] in "xX":
  57. i += 2
  58. while i < n and (expr[i].isdigit() or expr[i] in "abcdefABCDEF"):
  59. i += 1
  60. tokens.append(_Token(_TokenType.NUMBER, float(int(expr[start:i], 16))))
  61. continue
  62. # Dezimal / Float
  63. while i < n and expr[i].isdigit():
  64. i += 1
  65. if i < n and expr[i] == ".":
  66. i += 1
  67. while i < n and expr[i].isdigit():
  68. i += 1
  69. # Scientific Notation: 1e6, 2.5e-3
  70. if i < n and expr[i] in "eE":
  71. i += 1
  72. if i < n and expr[i] in "+-":
  73. i += 1
  74. while i < n and expr[i].isdigit():
  75. i += 1
  76. tokens.append(_Token(_TokenType.NUMBER, float(expr[start:i])))
  77. continue
  78. # Operatoren
  79. if ch == "+":
  80. tokens.append(_Token(_TokenType.PLUS))
  81. i += 1
  82. elif ch == "-":
  83. tokens.append(_Token(_TokenType.MINUS))
  84. i += 1
  85. elif ch == "*":
  86. tokens.append(_Token(_TokenType.STAR))
  87. i += 1
  88. elif ch == "/":
  89. tokens.append(_Token(_TokenType.SLASH))
  90. i += 1
  91. elif ch == "%":
  92. tokens.append(_Token(_TokenType.PERCENT))
  93. i += 1
  94. elif ch == "^":
  95. tokens.append(_Token(_TokenType.CARET))
  96. i += 1
  97. elif ch == "&":
  98. tokens.append(_Token(_TokenType.AMP))
  99. i += 1
  100. elif ch == "|":
  101. tokens.append(_Token(_TokenType.PIPE))
  102. i += 1
  103. elif ch == "~":
  104. tokens.append(_Token(_TokenType.TILDE))
  105. i += 1
  106. elif ch == "(":
  107. tokens.append(_Token(_TokenType.LPAREN))
  108. i += 1
  109. elif ch == ")":
  110. tokens.append(_Token(_TokenType.RPAREN))
  111. i += 1
  112. elif ch.isalpha():
  113. # Keywords: and, or, xor, not, nand, nor
  114. start = i
  115. while i < n and expr[i].isalpha():
  116. i += 1
  117. word = expr[start:i].lower()
  118. kw_map = {
  119. "and": _TokenType.KW_AND,
  120. "or": _TokenType.KW_OR,
  121. "xor": _TokenType.KW_XOR,
  122. "not": _TokenType.KW_NOT,
  123. "nand": _TokenType.KW_NAND,
  124. "nor": _TokenType.KW_NOR,
  125. }
  126. if word in kw_map:
  127. tokens.append(_Token(kw_map[word]))
  128. else:
  129. # Unbekanntes Wort — Fehler
  130. tokens.append(_Token(_TokenType.ERROR))
  131. else:
  132. tokens.append(_Token(_TokenType.ERROR))
  133. i += 1
  134. tokens.append(_Token(_TokenType.END))
  135. return tokens
  136. def _to_int(d: float) -> int:
  137. """Konvertiert float zu int fuer bitweise Operationen."""
  138. return round(d)
  139. class _Parser:
  140. """Recursive-Descent-Parser fuer mathematische Ausdruecke."""
  141. def __init__(self, tokens: list[_Token]) -> None:
  142. self._tokens = tokens
  143. self._pos = 0
  144. self.error = False
  145. def _peek(self) -> _Token:
  146. return self._tokens[self._pos]
  147. def _advance(self) -> _Token:
  148. tok = self._tokens[self._pos]
  149. self._pos += 1
  150. return tok
  151. def _expect(self, tt: _TokenType) -> bool:
  152. if self._peek().type == tt:
  153. self._advance()
  154. return True
  155. self.error = True
  156. return False
  157. def parse(self) -> float:
  158. """Parst den kompletten Ausdruck."""
  159. result = self._parse_nor()
  160. if self._peek().type != _TokenType.END:
  161. self.error = True
  162. return result
  163. # Praezedenz 1: NOR
  164. def _parse_nor(self) -> float:
  165. left = self._parse_nand()
  166. while self._peek().type == _TokenType.KW_NOR:
  167. self._advance()
  168. right = self._parse_nand()
  169. left = float(~(_to_int(left) | _to_int(right)))
  170. return left
  171. # Praezedenz 2: NAND
  172. def _parse_nand(self) -> float:
  173. left = self._parse_or()
  174. while self._peek().type == _TokenType.KW_NAND:
  175. self._advance()
  176. right = self._parse_or()
  177. left = float(~(_to_int(left) & _to_int(right)))
  178. return left
  179. # Praezedenz 3: OR / |
  180. def _parse_or(self) -> float:
  181. left = self._parse_xor()
  182. while self._peek().type in (_TokenType.PIPE, _TokenType.KW_OR):
  183. self._advance()
  184. right = self._parse_xor()
  185. left = float(_to_int(left) | _to_int(right))
  186. return left
  187. # Praezedenz 4: XOR
  188. def _parse_xor(self) -> float:
  189. left = self._parse_and()
  190. while self._peek().type == _TokenType.KW_XOR:
  191. self._advance()
  192. right = self._parse_and()
  193. left = float(_to_int(left) ^ _to_int(right))
  194. return left
  195. # Praezedenz 5: AND / &
  196. def _parse_and(self) -> float:
  197. left = self._parse_add()
  198. while self._peek().type in (_TokenType.AMP, _TokenType.KW_AND):
  199. self._advance()
  200. right = self._parse_add()
  201. left = float(_to_int(left) & _to_int(right))
  202. return left
  203. # Praezedenz 6: + -
  204. def _parse_add(self) -> float:
  205. left = self._parse_mul()
  206. while self._peek().type in (_TokenType.PLUS, _TokenType.MINUS):
  207. op = self._advance().type
  208. right = self._parse_mul()
  209. if op == _TokenType.PLUS:
  210. left += right
  211. else:
  212. left -= right
  213. return left
  214. # Praezedenz 7: * / %
  215. def _parse_mul(self) -> float:
  216. left = self._parse_pow()
  217. while self._peek().type in (_TokenType.STAR, _TokenType.SLASH, _TokenType.PERCENT):
  218. op = self._advance().type
  219. right = self._parse_pow()
  220. if op == _TokenType.STAR:
  221. left *= right
  222. elif op == _TokenType.SLASH:
  223. if right == 0:
  224. self.error = True
  225. return 0.0
  226. left /= right
  227. else:
  228. if right == 0:
  229. self.error = True
  230. return 0.0
  231. left = float(_to_int(left) % _to_int(right))
  232. return left
  233. # Praezedenz 8: ^ (rechtsassoziativ)
  234. def _parse_pow(self) -> float:
  235. base = self._parse_unary()
  236. if self._peek().type == _TokenType.CARET:
  237. self._advance()
  238. exp = self._parse_pow() # Rechtsassoziativ: Rekursion
  239. return math.pow(base, exp)
  240. return base
  241. # Praezedenz 9: unaer (- ~ not)
  242. def _parse_unary(self) -> float:
  243. if self._peek().type == _TokenType.MINUS:
  244. self._advance()
  245. return -self._parse_unary()
  246. if self._peek().type in (_TokenType.TILDE, _TokenType.KW_NOT):
  247. self._advance()
  248. return float(~_to_int(self._parse_unary()))
  249. return self._parse_primary()
  250. # Primaer: Zahl oder geklammert
  251. def _parse_primary(self) -> float:
  252. tok = self._peek()
  253. if tok.type == _TokenType.NUMBER:
  254. self._advance()
  255. return tok.value
  256. if tok.type == _TokenType.LPAREN:
  257. self._advance()
  258. val = self._parse_nor()
  259. self._expect(_TokenType.RPAREN)
  260. return val
  261. self.error = True
  262. self._advance()
  263. return 0.0
  264. def _format_result(value: float) -> str:
  265. """Formatiert das Ergebnis: Ganzzahlen ohne Dezimalpunkt."""
  266. if value == int(value) and abs(value) < 1e15:
  267. return str(int(value))
  268. # Bis zu 10 signifikante Stellen, trailing Zeros entfernen
  269. s = f"{value:.10g}"
  270. return s
  271. def eval_math(expr: str) -> str | None:
  272. """
  273. Wertet einen mathematischen Ausdruck aus.
  274. Args:
  275. expr: Der Ausdruck (ohne die (:...) Klammern).
  276. Returns:
  277. Das Ergebnis als String oder None bei Fehler.
  278. """
  279. tokens = _tokenize(expr.strip())
  280. parser = _Parser(tokens)
  281. result = parser.parse()
  282. if parser.error:
  283. return None
  284. return _format_result(result)