Skip to content
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*~
#*#
*.pyc
__pycache__/
49 changes: 49 additions & 0 deletions SimpleExpressionEvaluator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/python3

from fractions import Fraction
from expression_graph.expression_graph import ExpressionGraph, \
LeftmostEvaluatingExpressionGraph, EmptyExpressionException, \
IncompleteExpressionException
from expression_graph.operator import Operator

def simpleExpressionEvaluator(expression):
expr_strs = expression.split(' ')
expr_nodes = list()
if len(expr_strs) > 0 and expression != '':
for n in expr_strs:
node = None
if n in Operator.operators:
node = n
else:
try:
node = int(n)
except ValueError as e:
try:
node = float(n)
except ValueError as e2:
print('{0} is not a valid value for the expression parser'.format(n))
expr_nodes.append(node)
print(expr_nodes)
return LeftmostEvaluatingExpressionGraph(*expr_nodes)


if __name__ == '__main__':
print(simpleExpressionEvaluator('5').eval())
print(simpleExpressionEvaluator('2 + 2').eval())
print(simpleExpressionEvaluator('2 + 2.5').eval())
print(simpleExpressionEvaluator('2 + 2.5 * 23 / 42 - 8').eval())
print(simpleExpressionEvaluator('4 / 7').eval())
print(simpleExpressionEvaluator('4 / 7.1').eval())
print(simpleExpressionEvaluator('4 / 2').eval())

try:
print(simpleExpressionEvaluator('').eval())
except EmptyExpressionException as e:
print('empty expression')

try:
print(simpleExpressionEvaluator('4 /').eval())
except IncompleteExpressionException as e:
print('incomplete expression')


2 changes: 2 additions & 0 deletions expression_graph/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/python3

153 changes: 153 additions & 0 deletions expression_graph/expression_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/usr/bin/python3

from fractions import Fraction
from .operator import Operator, IntegralOperator, RationalOperator, \
RealOperator, UnsupportedOperatorException
from .operator_node import OperatorNode, UnsupportedNodeTypeException, \
InvalidOperatorException, InvalidValueException
from .expression_node import ExpressionNode
from .value_node import ValueNode, IntegralValueNode, RationalValueNode, \
RealValueNode, NodeValueTypeException

class InvalidGraphException(Exception):
pass

class EmptyExpressionException(Exception):
pass

class IncompleteExpressionException(Exception):
pass

class ExpressionGraph(object):
""" Class to represent an evaluable arithmetic expression as a graph."""
def __init__(self, graph):
if not isinstance(graph, ExpressionNode) and not isinstance(graph, ExpressionGraph):
raise InvalidGraphException()
else:
self._graph = graph

def depth(self):
if isinstance(self._graph, ExpressionNode):
return self._graph.depth()
else:
return self._graph._node.depth()

def __str__(self):
if self._graph is None:
return ''
elif isinstance(self._graph, ExpressionNode):
return str(self._graph)
else:
return str(self._graph._node)


class LeftmostEvaluatingExpressionGraph(ExpressionGraph):
""" Class to represent an evaluable arithmetic expression as a graph."""
def __init__(self, *elements, **kwargs):
head = None
if 'head' in kwargs.keys():
head = kwargs['head']

if elements is None or len(elements) == 0 or elements[0] is None:
raise EmptyExpressionException()
elif (head is None and (1 < len(elements) < 3)) or (head is not None and len(elements) < 2):
raise IncompleteExpressionException()

self._node = None
self._graph = None
lnode = None
opnode = None
rnode = None
next_element = IterableIndex(elements, IncompleteExpressionException)

# In order to get the desired order of operations, we need
# to built the graph bottom up, with a left skew - that is,
# the left-most elements must be built first, then the
# elements to the right are built in such a way that the final
# graph goes down (possibly) multiple levels to the left,
# but only one level to the right.

if head is None:
lvalue = elements[next_element.value()]
lnode = assignValueNodeType(lvalue)
try:
next_element.iterate()
except IncompleteExpressionException as e:
# in this instance, it means that there is only a
# single element in the expression, which is the value
# of the expression
self._node = lnode
self._graph = self
return

lnode = assignValueNodeType(lvalue)

# head is not empty
else:
lnode = head

opvalue = elements[next_element.value()]
next_element.iterate()
rvalue = elements[next_element.value()]
rnode = assignValueNodeType(rvalue)

self._node = OperatorNode(Operator(opvalue), lnode, rnode)
remaining_elements = None
try:
next_element.iterate()
remainder_start = next_element.value()
remaining_elements = elements[remainder_start:]
except IncompleteExpressionException as e:
pass
finally:
if remaining_elements is not None:
self._node = LeftmostEvaluatingExpressionGraph(*remaining_elements, head=self._node)
super().__init__(self._node)

def eval(self):
""" Evaluate the graph. This works because only the rightmost node of
the graph is actually exposed as the handle of the graph,
so all nodes are guaranteed to be evaluated in the correct order."""
if self._graph is None:
return None
elif isinstance(self._graph, ExpressionNode):
return self._graph.eval()
else:
return self._graph._node.eval()



def assignValueNodeType(value):
""" Utility function to map a value to its correct ValueNode type. """
val_type = type(value)
if val_type not in [int, Fraction, float]:
raise NodeValueTypeException('{0} is not a valid value for a node'.format(value))
else:
if val_type == int:
return IntegralValueNode(value)
elif val_type == Fraction:
return RationalValueNode(value)
else:
return RealValueNode(value)


class IterableIndex(object):
"""utility class for an index that automatically checks whether
it has overrun the end of the list it is bound to, and throws the
correct exception when it does."""

def __init__(self, iterated_list, raised_exception):
self._index = 0
self._iterated_list = iterated_list
self._raised_exception = raised_exception

def value(self):
return self._index

def iterate(self):
self._index += 1
if self._index >= len(self._iterated_list):
raise self._raised_exception()



12 changes: 12 additions & 0 deletions expression_graph/expression_node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/python3

class ExpressionNode(object):

def __init__(self, node):
self._node = node

def eval(self):
pass

def __str__(self):
return str(self._node)
61 changes: 61 additions & 0 deletions expression_graph/operator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/python3

from fractions import Fraction

class UnsupportedOperatorException(Exception):
pass

class Operator(str):
# class constants representing the possible values of an Operator
ADD = '+'
SUBTRACT = '-'
MULTIPLY = '*'
DIVIDE = '/'

operators = [ADD, SUBTRACT, MULTIPLY, DIVIDE]

def __init__(self, operator):
if operator not in Operator.operators:
raise UnsupportedOperatorException('{0} is not a supported operator.'
.format(operator))
else:
self._operator = operator

def apply(self, operand, operahend):
return self.operation.get(self._operator)(operand, operahend)

def __str__(self):
return self._operator



class IntegralOperator(Operator):
operation = {Operator.ADD: int.__add__,
Operator.SUBTRACT: int.__sub__,
Operator.MULTIPLY: int.__mul__,
Operator.DIVIDE: Fraction}

def __init__(self, operator):
super().__init__(operator)



class RationalOperator(Operator):
operation = {Operator.ADD: Fraction.__add__,
Operator.SUBTRACT: Fraction.__sub__,
Operator.MULTIPLY: Fraction.__mul__,
Operator.DIVIDE: Fraction.__truediv__}

def __init__(self, operator):
super().__init__(operator)



class RealOperator(Operator):
operation = {Operator.ADD: float.__add__,
Operator.SUBTRACT: float.__sub__,
Operator.MULTIPLY: float.__mul__,
Operator.DIVIDE: float.__truediv__}

def __init__(self, operator):
super().__init__(operator)
90 changes: 90 additions & 0 deletions expression_graph/operator_node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/python3

from fractions import Fraction
from .operator import Operator, IntegralOperator, RationalOperator, RealOperator
from .expression_node import ExpressionNode
from .value_node import ValueNode, IntegralValueNode, RationalValueNode, \
RealValueNode

class UnsupportedNodeTypeException(Exception):
pass

class InvalidOperatorException(Exception):
pass

class InvalidValueException(Exception):
pass


class OperatorNode(ExpressionNode):
nodeTypes = [RealValueNode, RationalValueNode, IntegralValueNode]

def __init__(self, operator, lnode, rnode):
if not isinstance(operator, Operator):
raise InvalidOperatorException('{0} is not a supported operator'.format(operator))
elif not isinstance(lnode, ExpressionNode):
print(type(lnode))
raise InvalidValueException('{0} is not a valid node'.format(lnode))
elif not isinstance(rnode, ExpressionNode):
raise InvalidValueException('{0} is not a valid node'.format(rnode))
else:
super().__init__(operator)
self._lnode = lnode
self._rnode = rnode

def eval(self):
opsym = str(self._node)
lvalue = self._lnode.eval()
rvalue = self._rnode.eval()
lvalue_type = type(lvalue)
rvalue_type = type(rvalue)

effective_operator = self._node
effective_lvalue = lvalue
effective_rvalue = rvalue

# this part is a bit concerning to me, as it scales poorly and is
# quite brittle. I'll try to find a more elegant solution later.

if lvalue_type == float:
effective_operator = RealOperator(opsym)
if rvalue_type == Fraction:
effective_rvalue = rvalue.numerator() // rvalue.denominator()
elif rvalue_type == int:
effective_rvalue = float(rvalue)
elif rvalue_type == float:
pass
else:
raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(lvalue_type))

elif lvalue_type == Fraction:
if rvalue_type == float:
effective_operator = RealOperator(opsym)
effective_lvalue = lvalue.numerator() // lvalue.denominator()
elif rvalue_type in [int, Fraction]:
effective_operator = RationalOperator(opsym)
else:
raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(lvalue_type))

elif lvalue_type == int:
if rvalue_type == float:
effective_operator = RealOperator(opsym)
effective_lvalue = float(lvalue)
elif rvalue_type == Fraction:
effective_operator = RationalOperator(opsym)
elif rvalue_type == int:
effective_operator = IntegralOperator(opsym)
else:
raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(rvalue_type))
else:
raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(lvalue_type))

return effective_operator.apply(effective_lvalue, effective_rvalue)


def __str__(self):
s = str(self._lnode) + ' ' + str(self._node) + ' ' + str(self._rnode)
return s

def depth(self):
return 1 + max(self._lnode.depth(), self._rnode.depth())
Loading