Refactored project to remove poetry. Pure pip used instead
This commit is contained in:
68
PyQt-Plotter-Dialog/parser/README.md
Normal file
68
PyQt-Plotter-Dialog/parser/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Math expression parser
|
||||
|
||||
Math expression evaluation library. It supports most of useful math operations and functions. Expressions can contain variables which can be substituted with `int`s, `float`s or `numpy.ndarray`s.
|
||||
|
||||
## Example usage
|
||||
|
||||
```python
|
||||
from parser import Parser
|
||||
|
||||
parser = Parser("(-b + sqrt(b^2-4a c))/(2a)")
|
||||
|
||||
parser.variables_names # {'c', 'a', 'b'}
|
||||
|
||||
parser.evaluate({"a": 1, "b": -3, "c": 2}) # 1.0
|
||||
|
||||
parser.evaluate({"a": [1, 1, 1], "b": [-5, -6, -9], "c": [6, 9, 20]}) # [2. 3. 4.]
|
||||
```
|
||||
## Expression syntax
|
||||
|
||||
Expression can contain numbers or variable names with functions applyed to them, separated with operators or united with braces.
|
||||
|
||||
Theese are supported:
|
||||
|
||||
Functions:
|
||||
|
||||
|name| math |
|
||||
|--|--|
|
||||
| `abs` | $\|x\|$ |
|
||||
| `acos` | $\cos^{-1}(x)$ |
|
||||
| `acosh` | $\cosh^{-1}(x)$ |
|
||||
| `acot` | $\cot^{-1}(x)$ |
|
||||
| `asin` | $\sin^{-1}(x)$ |
|
||||
| `asinh` | $\sinh^{-1}(x)$ |
|
||||
| `atan` | $\tan^{-1}(x)$ |
|
||||
| `avg` | $\overline X$ |
|
||||
| `cos` | $\cos(x)$ |
|
||||
| `cosh` | $\cosh(x)$ |
|
||||
| `cot` | $\cot(x)$ |
|
||||
| `exp` | $\exp(x)$ |
|
||||
| `inf` | $\inf(X)$ |
|
||||
| `lg` | $\lg(x)$ |
|
||||
| `ln` | $\ln(x)$ |
|
||||
| `log10` | $\log_{10}(x)$ |
|
||||
| `log2` | $\log_2(x)$ |
|
||||
| `max` | $\sup(X)$ |
|
||||
| `min` | $\inf(X)$ |
|
||||
| `prod` | $\displaystyle \prod_{i=0}^n x_i$ |
|
||||
| `sgn` | $sgn(x)$ |
|
||||
| `sign` | $sgn(x)$ |
|
||||
| `sin` | $\sin(x)$ |
|
||||
| `sinh` | $\sinh(x)$ |
|
||||
| `sqrt` | $\sqrt{x}$ |
|
||||
| `sum` | $\displaystyle\sum_{i=0}^n x_i$ |
|
||||
| `sup` | $\sup(X)$ |
|
||||
| `tan` | $\tan(x)$ |
|
||||
| `tanh` | $\tanh(x)$ |
|
||||
|
||||
Operators: `+`, `-`, `*`, `/`, `^`, `%`
|
||||
|
||||
Braces: `()`, `[]`, `{}`
|
||||
|
||||
Floating points: `.`, `,`
|
||||
|
||||
Functions have only one argument, provided in braces. Operators must have two operands except minus (if it is the first character of equation or braced expression).
|
||||
|
||||
`avg`, `sum`, `max`, `sup`, `min`, `inf` and `prod` applied on `numpy.ndarray` produce `float`.
|
||||
|
||||
**! There is no error handling yet !**
|
21
PyQt-Plotter-Dialog/parser/__init__.py
Normal file
21
PyQt-Plotter-Dialog/parser/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from .parser import (
|
||||
BinaryExpression,
|
||||
Expression,
|
||||
Operation,
|
||||
Parser,
|
||||
Token,
|
||||
Tokenizer,
|
||||
UnaryExpression,
|
||||
ValueExpression,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"BinaryExpression",
|
||||
"Expression",
|
||||
"Operation",
|
||||
"Parser",
|
||||
"Token",
|
||||
"Tokenizer",
|
||||
"UnaryExpression",
|
||||
"ValueExpression",
|
||||
)
|
16
PyQt-Plotter-Dialog/parser/__main__.py
Normal file
16
PyQt-Plotter-Dialog/parser/__main__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from . import Parser
|
||||
|
||||
expression = input("Input math expression: ")
|
||||
|
||||
parser = Parser(expression)
|
||||
|
||||
print("Variables in your expression: " + ", ".join(parser.variables_names))
|
||||
|
||||
variables = {}
|
||||
|
||||
for key in parser.variables_names:
|
||||
variables[key] = float(input(f"Input '{key}' variable value: "))
|
||||
|
||||
res = parser.evaluate(variables)
|
||||
|
||||
print(f"Evaluation result is: {res}")
|
6
PyQt-Plotter-Dialog/parser/constants.py
Normal file
6
PyQt-Plotter-Dialog/parser/constants.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import numpy as np
|
||||
|
||||
CONSTANTS = {
|
||||
"e": np.e,
|
||||
"pi": np.pi,
|
||||
}
|
52
PyQt-Plotter-Dialog/parser/expression.py
Normal file
52
PyQt-Plotter-Dialog/parser/expression.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import abc
|
||||
from collections.abc import Callable, Mapping
|
||||
|
||||
from .types import FunctionType, OperatorType, ValueType
|
||||
|
||||
|
||||
class Expression(abc.ABC):
|
||||
_evaluator: Callable[[Mapping[str, ValueType]], ValueType]
|
||||
|
||||
def evaluate(self, variables: Mapping[str, ValueType]):
|
||||
return self._evaluator(variables)
|
||||
|
||||
|
||||
class ValueExpression(Expression):
|
||||
def __init__(self, a: str | ValueType):
|
||||
self.__debug_a = a
|
||||
|
||||
if isinstance(a, str):
|
||||
self._evaluator = lambda vars: vars[a]
|
||||
else:
|
||||
self._evaluator = lambda _: a
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__debug_a}>"
|
||||
|
||||
|
||||
class UnaryExpression(Expression):
|
||||
def __init__(self, function: FunctionType, a: Expression):
|
||||
self.__debug_f = function.__name__
|
||||
self.__debug_a = repr(a)
|
||||
|
||||
self._evaluator = lambda vars: function(a.evaluate(vars))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__debug_f}({self.__debug_a})>"
|
||||
|
||||
|
||||
class BinaryExpression(Expression):
|
||||
def __init__(
|
||||
self,
|
||||
function: OperatorType,
|
||||
a: Expression,
|
||||
b: Expression,
|
||||
):
|
||||
self.__debug_f = function.__name__
|
||||
self.__debug_a = repr(a)
|
||||
self.__debug_b = repr(b)
|
||||
|
||||
self._evaluator = lambda vars: function(a.evaluate(vars), b.evaluate(vars))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__debug_a} {self.__debug_f} {self.__debug_b}>"
|
89
PyQt-Plotter-Dialog/parser/operation.py
Normal file
89
PyQt-Plotter-Dialog/parser/operation.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .types import FunctionType, OperatorType, ValueType
|
||||
|
||||
|
||||
def acot(x: ValueType):
|
||||
return np.arctan(1 / x)
|
||||
|
||||
|
||||
def cot(x: ValueType):
|
||||
return 1 / np.tan(x)
|
||||
|
||||
|
||||
functions: dict[str, FunctionType] = {
|
||||
"abs": np.abs,
|
||||
"acos": np.arccos,
|
||||
"acosh": np.arccosh,
|
||||
"acot": acot,
|
||||
"asin": np.arcsin,
|
||||
"asinh": np.arcsinh,
|
||||
"atan": np.arctan,
|
||||
"avg": np.average,
|
||||
"cos": np.cos,
|
||||
"cosh": np.cosh,
|
||||
"cot": cot,
|
||||
"exp": np.exp,
|
||||
"inf": np.inf,
|
||||
"lg": np.log10,
|
||||
"ln": np.log,
|
||||
"log10": np.log10,
|
||||
"log2": np.log2,
|
||||
"max": np.max,
|
||||
"min": np.min,
|
||||
"prod": np.prod,
|
||||
"sgn": np.sign,
|
||||
"sign": np.sign,
|
||||
"sin": np.sin,
|
||||
"sinh": np.sinh,
|
||||
"sqrt": np.sqrt,
|
||||
"sum": np.sum,
|
||||
"sup": np.max,
|
||||
"tanh": np.tanh,
|
||||
"tan": np.tan,
|
||||
}
|
||||
|
||||
operators: dict[str, OperatorType] = {
|
||||
"+": np.add,
|
||||
"-": np.subtract,
|
||||
"*": np.multiply,
|
||||
"/": np.divide,
|
||||
"%": np.mod,
|
||||
"^": np.float_power,
|
||||
}
|
||||
|
||||
priorities: dict[str, int] = {
|
||||
"(": 0,
|
||||
"+": 1,
|
||||
"-": 1,
|
||||
"*": 2,
|
||||
"/": 2,
|
||||
"%": 2,
|
||||
"^": 3,
|
||||
"f": 4, # function
|
||||
")": 5,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Operation:
|
||||
evaluator: (FunctionType | OperatorType | str)
|
||||
priority: int
|
||||
size: int
|
||||
|
||||
|
||||
class FunctionOperation(Operation):
|
||||
def __init__(self, name: str):
|
||||
super().__init__(functions[name], priorities["f"], 1)
|
||||
|
||||
|
||||
class BraceOperation(Operation):
|
||||
def __init__(self, name: str):
|
||||
super().__init__(name, priorities[name], 0)
|
||||
|
||||
|
||||
class OperatorOperation(Operation):
|
||||
def __init__(self, name: str):
|
||||
super().__init__(operators[name], priorities[name], 2)
|
81
PyQt-Plotter-Dialog/parser/parser.py
Normal file
81
PyQt-Plotter-Dialog/parser/parser.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from .expression import BinaryExpression, Expression, UnaryExpression, ValueExpression
|
||||
from .operation import (
|
||||
BraceOperation,
|
||||
FunctionOperation,
|
||||
Operation,
|
||||
OperatorOperation,
|
||||
priorities,
|
||||
)
|
||||
from .tokenizer import Token, Tokenizer
|
||||
from .types import ValueType
|
||||
from .constants import CONSTANTS
|
||||
|
||||
|
||||
class Parser:
|
||||
def __init__(self, input_expr: str):
|
||||
self.input_expr = input_expr
|
||||
self.variables_names: set[str] = set()
|
||||
|
||||
self.tokenize()
|
||||
self.parse()
|
||||
|
||||
def tokenize(self):
|
||||
self.tokens = Tokenizer(self.input_expr)
|
||||
|
||||
def parse(self):
|
||||
self.val_stack: list[Expression] = []
|
||||
self.op_stack: list[Operation] = []
|
||||
|
||||
for t_val, t_type in self.tokens:
|
||||
if t_type in (Token.Number, Token.Variable):
|
||||
self.val_stack.append(ValueExpression(t_val))
|
||||
|
||||
if t_type == Token.Variable:
|
||||
self.variables_names.add(t_val)
|
||||
|
||||
elif t_type == Token.Function:
|
||||
self.op_stack.append(FunctionOperation(t_val))
|
||||
|
||||
elif t_type == Token.LBrace:
|
||||
self.op_stack.append(BraceOperation("("))
|
||||
|
||||
elif t_type == Token.RBrace:
|
||||
while len(self.op_stack) > 0 and not (
|
||||
self.op_stack[-1].size == 0 and self.op_stack[-1].priority == 0
|
||||
): # until next in stack is lbrace
|
||||
self.do_one()
|
||||
self.op_stack.pop() # pop lbrace
|
||||
|
||||
elif t_type == Token.Operator:
|
||||
t_priority = priorities[t_val]
|
||||
|
||||
while (
|
||||
len(self.op_stack) > 0 and self.op_stack[-1].priority > t_priority
|
||||
):
|
||||
self.do_one()
|
||||
|
||||
self.op_stack.append(OperatorOperation(t_val))
|
||||
|
||||
while len(self.op_stack) > 0:
|
||||
self.do_one()
|
||||
|
||||
self._evaluator = self.val_stack[0].evaluate
|
||||
|
||||
self.__debug_expr = repr(self.val_stack)
|
||||
|
||||
def evaluate(self, variables: dict[str, ValueType]):
|
||||
variables |= CONSTANTS
|
||||
return self._evaluator(variables)
|
||||
|
||||
def do_one(self):
|
||||
op = self.op_stack.pop()
|
||||
|
||||
if op.size == 1:
|
||||
a = self.val_stack.pop()
|
||||
self.val_stack.append(UnaryExpression(op.evaluator, a))
|
||||
elif op.size == 2:
|
||||
b, a = self.val_stack.pop(), self.val_stack.pop() # inversed pop order
|
||||
self.val_stack.append(BinaryExpression(op.evaluator, a, b))
|
||||
|
||||
def __repr__(self):
|
||||
return self.__debug_expr
|
108
PyQt-Plotter-Dialog/parser/tokenizer.py
Normal file
108
PyQt-Plotter-Dialog/parser/tokenizer.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from .operation import functions, operators
|
||||
from .types import Token, TokenType
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tokenizer:
|
||||
expression: str
|
||||
|
||||
def __iter__(self) -> Generator[TokenType, None, None]:
|
||||
accumulator = ""
|
||||
prev = None
|
||||
|
||||
for ch in self.expression:
|
||||
if (breaker_type := Tokenizer.is_breaker(ch)) is not None:
|
||||
if len(accumulator) > 0:
|
||||
# ch is `(` after function name
|
||||
if breaker_type == Token.LBrace and Tokenizer.is_function(
|
||||
accumulator
|
||||
):
|
||||
yield accumulator, Token.Function
|
||||
prev = Token.Function
|
||||
accumulator = ""
|
||||
else:
|
||||
value, token_type = Tokenizer.detect_number(accumulator)
|
||||
yield value, token_type
|
||||
prev = token_type
|
||||
accumulator = ""
|
||||
|
||||
# `(` after variable or number
|
||||
if breaker_type == Token.LBrace:
|
||||
yield "*", Token.Operator
|
||||
prev = Token.Operator
|
||||
|
||||
# Unary minus case
|
||||
if ch == "-" and (prev == Token.LBrace or prev is None):
|
||||
yield 0, Token.Number
|
||||
prev = Token.Number
|
||||
|
||||
# `(expr)(expr)` case
|
||||
if breaker_type == Token.LBrace and prev == Token.RBrace:
|
||||
yield "*", Token.Operator
|
||||
prev = Token.Operator
|
||||
|
||||
if breaker_type != Token.Space:
|
||||
yield ch, breaker_type
|
||||
prev = breaker_type
|
||||
else:
|
||||
# Variable or function name after braced expr or variable and space
|
||||
if prev == Token.RBrace or prev == Token.Variable:
|
||||
yield "*", Token.Operator
|
||||
prev = Token.Operator
|
||||
|
||||
# Floating point number
|
||||
if ch in ",.":
|
||||
accumulator += "."
|
||||
continue
|
||||
|
||||
# Variable or function name after number
|
||||
if (
|
||||
not ch.isdecimal()
|
||||
and (num := Tokenizer.is_number(accumulator)) is not None
|
||||
):
|
||||
yield num, Token.Number
|
||||
yield "*", Token.Operator
|
||||
prev = Token.Operator
|
||||
accumulator = ""
|
||||
|
||||
accumulator += ch
|
||||
if len(accumulator) > 0:
|
||||
yield self.detect_number(accumulator)
|
||||
|
||||
@staticmethod
|
||||
def is_breaker(character) -> Optional[Token]:
|
||||
if character in operators:
|
||||
return Token.Operator
|
||||
if character in "([{":
|
||||
return Token.LBrace
|
||||
if character in ")]}":
|
||||
return Token.RBrace
|
||||
if character == " ":
|
||||
return Token.Space
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_number(string) -> Optional[float]:
|
||||
try:
|
||||
return float(string)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def detect_number(string) -> TokenType:
|
||||
if (num := Tokenizer.is_number(string)) is not None:
|
||||
return num, Token.Number
|
||||
else:
|
||||
return string, Token.Variable
|
||||
|
||||
@staticmethod
|
||||
def is_function(lexeme: str) -> bool:
|
||||
if lexeme in functions:
|
||||
return True
|
||||
|
||||
return False
|
23
PyQt-Plotter-Dialog/parser/types.py
Normal file
23
PyQt-Plotter-Dialog/parser/types.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from collections.abc import Callable
|
||||
from enum import Enum, auto
|
||||
|
||||
import numpy as np
|
||||
|
||||
ValueType = int | float | np.ndarray
|
||||
|
||||
FunctionType = Callable[[ValueType], ValueType]
|
||||
|
||||
OperatorType = Callable[[ValueType, ValueType], ValueType]
|
||||
|
||||
|
||||
class Token(Enum):
|
||||
Variable = auto()
|
||||
Number = auto()
|
||||
Function = auto()
|
||||
Operator = auto()
|
||||
LBrace = auto()
|
||||
RBrace = auto()
|
||||
Space = auto()
|
||||
|
||||
|
||||
TokenType = tuple[str | float, Token]
|
Reference in New Issue
Block a user