Documented parser package files
This commit is contained in:
parent
02f17d2e51
commit
59bb5d901f
@ -1,3 +1,7 @@
|
||||
"""
|
||||
Contains classes for expression tree representation and evaluation
|
||||
"""
|
||||
|
||||
import abc
|
||||
from collections.abc import Callable, Mapping
|
||||
|
||||
@ -5,6 +9,13 @@ from .types import FunctionType, OperatorType, ValueType
|
||||
|
||||
|
||||
class Expression(abc.ABC):
|
||||
"""
|
||||
Abstract base class for a single parsed expression as a tree data
|
||||
structure. It also defines its public function for triggering
|
||||
evaluation. Each child class sets `_evaluator` property to a
|
||||
function that accepts variables values and produces numpy array.
|
||||
"""
|
||||
|
||||
_evaluator: Callable[[Mapping[str, ValueType]], ValueType]
|
||||
|
||||
def evaluate(self, variables: Mapping[str, ValueType]):
|
||||
@ -12,6 +23,12 @@ class Expression(abc.ABC):
|
||||
|
||||
|
||||
class ValueExpression(Expression):
|
||||
"""
|
||||
This expression accepts variable name, numpy array or scalar number
|
||||
and evaluates to either constant or variable value, corresponding
|
||||
to its name.
|
||||
"""
|
||||
|
||||
def __init__(self, a: str | ValueType):
|
||||
self.__debug_a = a
|
||||
|
||||
@ -25,6 +42,12 @@ class ValueExpression(Expression):
|
||||
|
||||
|
||||
class UnaryExpression(Expression):
|
||||
"""
|
||||
This expression accepts function with one argument and `Expression`
|
||||
(value, unary or binary) and that function on expression, passed into it.
|
||||
It is applied for named functions like `exp(a)`
|
||||
"""
|
||||
|
||||
def __init__(self, function: FunctionType, a: Expression):
|
||||
self.__debug_f = function.__name__
|
||||
self.__debug_a = repr(a)
|
||||
@ -36,6 +59,12 @@ class UnaryExpression(Expression):
|
||||
|
||||
|
||||
class BinaryExpression(Expression):
|
||||
"""
|
||||
This expression is similar to `UnaryExpression`, but accepts function
|
||||
with two arguments and two expressions.
|
||||
It is applied for math operators like `a - b`
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
function: OperatorType,
|
||||
|
@ -5,6 +5,9 @@ import numpy as np
|
||||
from .types import FunctionType, OperatorType, ValueType
|
||||
|
||||
|
||||
# Additional functions that are not defined in numpy
|
||||
|
||||
|
||||
def acot(x: ValueType):
|
||||
return np.arctan(1 / x)
|
||||
|
||||
@ -13,6 +16,9 @@ def cot(x: ValueType):
|
||||
return 1 / np.tan(x)
|
||||
|
||||
|
||||
# Function and operation names to evaluators mapping
|
||||
|
||||
|
||||
functions: dict[str, FunctionType] = {
|
||||
"abs": np.abs,
|
||||
"acos": np.arccos,
|
||||
@ -69,21 +75,39 @@ priorities: dict[str, int] = {
|
||||
|
||||
@dataclass
|
||||
class Operation:
|
||||
"""
|
||||
Base class for math operation token (function, brace, operator).
|
||||
It stores the way it is evaluated, evaluation priority and number
|
||||
of arguments it supports.
|
||||
"""
|
||||
|
||||
evaluator: (FunctionType | OperatorType | str)
|
||||
priority: int
|
||||
size: int
|
||||
|
||||
|
||||
class FunctionOperation(Operation):
|
||||
"""
|
||||
`Operator` class factory that represents function
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
super().__init__(functions[name], priorities["f"], 1)
|
||||
|
||||
|
||||
class BraceOperation(Operation):
|
||||
"""
|
||||
`Operator` class factory that represents brace
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
super().__init__(name, priorities[name], 0)
|
||||
|
||||
|
||||
class OperatorOperation(Operation):
|
||||
"""
|
||||
`Operator` class factory that represents binary operator
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
super().__init__(operators[name], priorities[name], 2)
|
||||
|
@ -12,17 +12,32 @@ from .constants import CONSTANTS
|
||||
|
||||
|
||||
class Parser:
|
||||
"""
|
||||
Class that accepts math expression in its constructor,
|
||||
parses it and provides `evaluate` method to substitue
|
||||
variables values to it
|
||||
"""
|
||||
|
||||
def __init__(self, input_expr: str):
|
||||
self.input_expr = input_expr
|
||||
self.variables_names: set[str] = set()
|
||||
|
||||
self.tokenize()
|
||||
self.parse()
|
||||
self._tokenize()
|
||||
self._parse()
|
||||
|
||||
def tokenize(self):
|
||||
def _tokenize(self):
|
||||
"""
|
||||
Uses `Tokenizer` class for math expression splitting
|
||||
"""
|
||||
self.tokens = Tokenizer(self.input_expr)
|
||||
|
||||
def parse(self):
|
||||
def _parse(self):
|
||||
"""
|
||||
Generates an evaluation tree from tokens by utilizing two
|
||||
stacks - one for values and one for operations. Values and
|
||||
operations are placed into corresponding stacks, and when
|
||||
possible, assempled into tree node.
|
||||
"""
|
||||
self.val_stack: list[Expression] = []
|
||||
self.op_stack: list[Operation] = []
|
||||
|
||||
@ -43,7 +58,7 @@ class Parser:
|
||||
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._do_one()
|
||||
self.op_stack.pop() # pop lbrace
|
||||
|
||||
elif t_type == Token.Operator:
|
||||
@ -52,22 +67,22 @@ class Parser:
|
||||
while (
|
||||
len(self.op_stack) > 0 and self.op_stack[-1].priority > t_priority
|
||||
):
|
||||
self.do_one()
|
||||
self._do_one()
|
||||
|
||||
self.op_stack.append(OperatorOperation(t_val))
|
||||
|
||||
while len(self.op_stack) > 0:
|
||||
self.do_one()
|
||||
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):
|
||||
def _do_one(self):
|
||||
"""
|
||||
Assembles one operation into `Expression` tree node that is stored
|
||||
on value stack.
|
||||
"""
|
||||
op = self.op_stack.pop()
|
||||
|
||||
if op.size == 1:
|
||||
@ -77,5 +92,13 @@ class Parser:
|
||||
b, a = self.val_stack.pop(), self.val_stack.pop() # inversed pop order
|
||||
self.val_stack.append(BinaryExpression(op.evaluator, a, b))
|
||||
|
||||
def evaluate(self, variables: dict[str, ValueType]):
|
||||
"""
|
||||
Evaluates supplied to constructor expression with provided
|
||||
variables values
|
||||
"""
|
||||
variables |= CONSTANTS
|
||||
return self._evaluator(variables)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__debug_expr
|
||||
|
@ -8,6 +8,12 @@ from .types import Token, TokenType
|
||||
|
||||
@dataclass
|
||||
class Tokenizer:
|
||||
"""
|
||||
Implements an iterator that yields `Token`s one by one.
|
||||
It's grammatics is context-sensitive, but respects only a
|
||||
single previous token.
|
||||
"""
|
||||
|
||||
expression: str
|
||||
|
||||
def __iter__(self) -> Generator[TokenType, None, None]:
|
||||
|
Loading…
x
Reference in New Issue
Block a user