Refactored project to remove poetry. Pure pip used instead

This commit is contained in:
2023-10-24 13:48:24 +03:00
parent eb6309582e
commit e2b9025bc5
36 changed files with 106 additions and 2077 deletions

View 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 !**

View 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",
)

View 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}")

View File

@@ -0,0 +1,6 @@
import numpy as np
CONSTANTS = {
"e": np.e,
"pi": np.pi,
}

View 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}>"

View 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)

View 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

View 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

View 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]