| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- # -*- coding: utf-8 -*-
- """
- Mathematischer Ausdruck-Evaluator (Recursive Descent Parser).
- Wertet Ausdruecke der Form (:expression) aus.
- Unterstuetzt: +, -, *, /, %, ^ (Potenz), &, |, xor, ~, not, and, or, nand, nor.
- Klammern fuer Gruppierung. Hex-Literale (0xFF). Float/Int.
- Praezedenz (niedrig → hoch):
- nor → nand → | / or → xor → & / and → + - → * / % → ^ → unaer (- ~ not)
- Portiert von PrrintText_cpp/format.cpp (matheval Namespace).
- """
- from __future__ import annotations
- import math
- from enum import Enum, auto
- from typing import Any
- class _TokenType(Enum):
- NUMBER = auto()
- PLUS = auto()
- MINUS = auto()
- STAR = auto()
- SLASH = auto()
- PERCENT = auto()
- CARET = auto() # ^
- AMP = auto() # &
- PIPE = auto() # |
- TILDE = auto() # ~
- LPAREN = auto()
- RPAREN = auto()
- KW_AND = auto()
- KW_OR = auto()
- KW_XOR = auto()
- KW_NOT = auto()
- KW_NAND = auto()
- KW_NOR = auto()
- END = auto()
- ERROR = auto()
- class _Token:
- __slots__ = ("type", "value")
- def __init__(self, tt: _TokenType, value: float = 0.0) -> None:
- self.type = tt
- self.value = value
- def _tokenize(expr: str) -> list[_Token]:
- """Tokenisiert einen mathematischen Ausdruck."""
- tokens: list[_Token] = []
- i = 0
- n = len(expr)
- while i < n:
- ch = expr[i]
- # Whitespace ueberspringen
- if ch in " \t\r\n":
- i += 1
- continue
- # Zahlen (int, float, hex, scientific)
- if ch.isdigit() or (ch == "." and i + 1 < n and expr[i + 1].isdigit()):
- start = i
- # Hex: 0x...
- if ch == "0" and i + 1 < n and expr[i + 1] in "xX":
- i += 2
- while i < n and (expr[i].isdigit() or expr[i] in "abcdefABCDEF"):
- i += 1
- tokens.append(_Token(_TokenType.NUMBER, float(int(expr[start:i], 16))))
- continue
- # Dezimal / Float
- while i < n and expr[i].isdigit():
- i += 1
- if i < n and expr[i] == ".":
- i += 1
- while i < n and expr[i].isdigit():
- i += 1
- # Scientific Notation: 1e6, 2.5e-3
- if i < n and expr[i] in "eE":
- i += 1
- if i < n and expr[i] in "+-":
- i += 1
- while i < n and expr[i].isdigit():
- i += 1
- tokens.append(_Token(_TokenType.NUMBER, float(expr[start:i])))
- continue
- # Operatoren
- if ch == "+":
- tokens.append(_Token(_TokenType.PLUS))
- i += 1
- elif ch == "-":
- tokens.append(_Token(_TokenType.MINUS))
- i += 1
- elif ch == "*":
- tokens.append(_Token(_TokenType.STAR))
- i += 1
- elif ch == "/":
- tokens.append(_Token(_TokenType.SLASH))
- i += 1
- elif ch == "%":
- tokens.append(_Token(_TokenType.PERCENT))
- i += 1
- elif ch == "^":
- tokens.append(_Token(_TokenType.CARET))
- i += 1
- elif ch == "&":
- tokens.append(_Token(_TokenType.AMP))
- i += 1
- elif ch == "|":
- tokens.append(_Token(_TokenType.PIPE))
- i += 1
- elif ch == "~":
- tokens.append(_Token(_TokenType.TILDE))
- i += 1
- elif ch == "(":
- tokens.append(_Token(_TokenType.LPAREN))
- i += 1
- elif ch == ")":
- tokens.append(_Token(_TokenType.RPAREN))
- i += 1
- elif ch.isalpha():
- # Keywords: and, or, xor, not, nand, nor
- start = i
- while i < n and expr[i].isalpha():
- i += 1
- word = expr[start:i].lower()
- kw_map = {
- "and": _TokenType.KW_AND,
- "or": _TokenType.KW_OR,
- "xor": _TokenType.KW_XOR,
- "not": _TokenType.KW_NOT,
- "nand": _TokenType.KW_NAND,
- "nor": _TokenType.KW_NOR,
- }
- if word in kw_map:
- tokens.append(_Token(kw_map[word]))
- else:
- # Unbekanntes Wort — Fehler
- tokens.append(_Token(_TokenType.ERROR))
- else:
- tokens.append(_Token(_TokenType.ERROR))
- i += 1
- tokens.append(_Token(_TokenType.END))
- return tokens
- def _to_int(d: float) -> int:
- """Konvertiert float zu int fuer bitweise Operationen."""
- return round(d)
- class _Parser:
- """Recursive-Descent-Parser fuer mathematische Ausdruecke."""
- def __init__(self, tokens: list[_Token]) -> None:
- self._tokens = tokens
- self._pos = 0
- self.error = False
- def _peek(self) -> _Token:
- return self._tokens[self._pos]
- def _advance(self) -> _Token:
- tok = self._tokens[self._pos]
- self._pos += 1
- return tok
- def _expect(self, tt: _TokenType) -> bool:
- if self._peek().type == tt:
- self._advance()
- return True
- self.error = True
- return False
- def parse(self) -> float:
- """Parst den kompletten Ausdruck."""
- result = self._parse_nor()
- if self._peek().type != _TokenType.END:
- self.error = True
- return result
- # Praezedenz 1: NOR
- def _parse_nor(self) -> float:
- left = self._parse_nand()
- while self._peek().type == _TokenType.KW_NOR:
- self._advance()
- right = self._parse_nand()
- left = float(~(_to_int(left) | _to_int(right)))
- return left
- # Praezedenz 2: NAND
- def _parse_nand(self) -> float:
- left = self._parse_or()
- while self._peek().type == _TokenType.KW_NAND:
- self._advance()
- right = self._parse_or()
- left = float(~(_to_int(left) & _to_int(right)))
- return left
- # Praezedenz 3: OR / |
- def _parse_or(self) -> float:
- left = self._parse_xor()
- while self._peek().type in (_TokenType.PIPE, _TokenType.KW_OR):
- self._advance()
- right = self._parse_xor()
- left = float(_to_int(left) | _to_int(right))
- return left
- # Praezedenz 4: XOR
- def _parse_xor(self) -> float:
- left = self._parse_and()
- while self._peek().type == _TokenType.KW_XOR:
- self._advance()
- right = self._parse_and()
- left = float(_to_int(left) ^ _to_int(right))
- return left
- # Praezedenz 5: AND / &
- def _parse_and(self) -> float:
- left = self._parse_add()
- while self._peek().type in (_TokenType.AMP, _TokenType.KW_AND):
- self._advance()
- right = self._parse_add()
- left = float(_to_int(left) & _to_int(right))
- return left
- # Praezedenz 6: + -
- def _parse_add(self) -> float:
- left = self._parse_mul()
- while self._peek().type in (_TokenType.PLUS, _TokenType.MINUS):
- op = self._advance().type
- right = self._parse_mul()
- if op == _TokenType.PLUS:
- left += right
- else:
- left -= right
- return left
- # Praezedenz 7: * / %
- def _parse_mul(self) -> float:
- left = self._parse_pow()
- while self._peek().type in (_TokenType.STAR, _TokenType.SLASH, _TokenType.PERCENT):
- op = self._advance().type
- right = self._parse_pow()
- if op == _TokenType.STAR:
- left *= right
- elif op == _TokenType.SLASH:
- if right == 0:
- self.error = True
- return 0.0
- left /= right
- else:
- if right == 0:
- self.error = True
- return 0.0
- left = float(_to_int(left) % _to_int(right))
- return left
- # Praezedenz 8: ^ (rechtsassoziativ)
- def _parse_pow(self) -> float:
- base = self._parse_unary()
- if self._peek().type == _TokenType.CARET:
- self._advance()
- exp = self._parse_pow() # Rechtsassoziativ: Rekursion
- return math.pow(base, exp)
- return base
- # Praezedenz 9: unaer (- ~ not)
- def _parse_unary(self) -> float:
- if self._peek().type == _TokenType.MINUS:
- self._advance()
- return -self._parse_unary()
- if self._peek().type in (_TokenType.TILDE, _TokenType.KW_NOT):
- self._advance()
- return float(~_to_int(self._parse_unary()))
- return self._parse_primary()
- # Primaer: Zahl oder geklammert
- def _parse_primary(self) -> float:
- tok = self._peek()
- if tok.type == _TokenType.NUMBER:
- self._advance()
- return tok.value
- if tok.type == _TokenType.LPAREN:
- self._advance()
- val = self._parse_nor()
- self._expect(_TokenType.RPAREN)
- return val
- self.error = True
- self._advance()
- return 0.0
- def _format_result(value: float) -> str:
- """Formatiert das Ergebnis: Ganzzahlen ohne Dezimalpunkt."""
- if value == int(value) and abs(value) < 1e15:
- return str(int(value))
- # Bis zu 10 signifikante Stellen, trailing Zeros entfernen
- s = f"{value:.10g}"
- return s
- def eval_math(expr: str) -> str | None:
- """
- Wertet einen mathematischen Ausdruck aus.
- Args:
- expr: Der Ausdruck (ohne die (:...) Klammern).
- Returns:
- Das Ergebnis als String oder None bei Fehler.
- """
- tokens = _tokenize(expr.strip())
- parser = _Parser(tokens)
- result = parser.parse()
- if parser.error:
- return None
- return _format_result(result)
|