diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ee8fa2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*~ +#*# +*.pyc +__pycache__/ \ No newline at end of file diff --git a/SimpleExpressionEvaluator.py b/SimpleExpressionEvaluator.py new file mode 100755 index 0000000..94705ec --- /dev/null +++ b/SimpleExpressionEvaluator.py @@ -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') + + diff --git a/expression_graph/__init__.py b/expression_graph/__init__.py new file mode 100644 index 0000000..dc4d1b5 --- /dev/null +++ b/expression_graph/__init__.py @@ -0,0 +1,2 @@ +#!/usr/python3 + diff --git a/expression_graph/expression_graph.py b/expression_graph/expression_graph.py new file mode 100644 index 0000000..1ba20e5 --- /dev/null +++ b/expression_graph/expression_graph.py @@ -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() + + + diff --git a/expression_graph/expression_node.py b/expression_graph/expression_node.py new file mode 100644 index 0000000..2602b01 --- /dev/null +++ b/expression_graph/expression_node.py @@ -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) diff --git a/expression_graph/operator.py b/expression_graph/operator.py new file mode 100644 index 0000000..3926c56 --- /dev/null +++ b/expression_graph/operator.py @@ -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) diff --git a/expression_graph/operator_node.py b/expression_graph/operator_node.py new file mode 100644 index 0000000..e5e159d --- /dev/null +++ b/expression_graph/operator_node.py @@ -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()) diff --git a/expression_graph/tests/test_expression_graph.py b/expression_graph/tests/test_expression_graph.py new file mode 100644 index 0000000..5e1aff5 --- /dev/null +++ b/expression_graph/tests/test_expression_graph.py @@ -0,0 +1,63 @@ +#!/usr/bin/python3 + +from fractions import Fraction +from ..expression_graph import ExpressionGraph, \ + LeftmostEvaluatingExpressionGraph, EmptyExpressionException, \ + IncompleteExpressionException + +from unittest import TestCase + +class ExpressionGraphTest(TestCase): + def testEmptyExpression(self): + self.assertRaises(EmptyExpressionException, LeftmostEvaluatingExpressionGraph) + + def testSingleValueExpression(self): + graph = LeftmostEvaluatingExpressionGraph(2) + self.assertEqual('2', str(graph)) + self.assertEqual(2, graph.eval()) + + def testSimpleExpression(self): + graph = LeftmostEvaluatingExpressionGraph(2, '+', 2) + self.assertEqual('2 + 2', str(graph)) + self.assertEqual(4, graph.eval()) + + def testCompoundExpression(self): + graph = LeftmostEvaluatingExpressionGraph(2, '+', 2, '-', 6) + self.assertEqual('2 + 2 - 6', str(graph)) + self.assertEqual(-2, graph.eval()) + + def testDivisionExpression(self): + graph = LeftmostEvaluatingExpressionGraph(4, '/', 2) + self.assertEqual('4 / 2', str(graph)) + self.assertEqual(Fraction(4, 2), graph.eval()) + + def testFourValueExpression(self): + graph = LeftmostEvaluatingExpressionGraph(2, '+', 2, '-', 6, '*', 4) + self.assertEqual('2 + 2 - 6 * 4', str(graph)) + self.assertEqual(-8, graph.eval()) + + def testExtendedExpression(self): + graph = LeftmostEvaluatingExpressionGraph(2, '+', 2, '-', 6, '*', 4, '/', 2) + self.assertEqual('2 + 2 - 6 * 4 / 2', str(graph)) + self.assertEqual(-4, graph.eval()) + + def testCaptureIncompleteExpression(self): + self.assertRaises(IncompleteExpressionException, LeftmostEvaluatingExpressionGraph, 2, '+', 2, '-', 6, '*', 4, '/') + + + def testExtendedWithFractionalResultExpression(self): + graph = LeftmostEvaluatingExpressionGraph(2, '+', 2, '-', 6, '*', 4, '/', 13) + self.assertEqual('2 + 2 - 6 * 4 / 13', str(graph)) + self.assertEqual(Fraction(-8, 13), graph.eval()) + + + def testExtendedWithRealResultExpression(self): + graph = LeftmostEvaluatingExpressionGraph(2, '+', 2.5, '-', 6, '*', 4, '/', 13) + self.assertEqual('2 + 2.5 - 6 * 4 / 13', str(graph)) + result = ((2 + 2.5 - 6) * 4) / 13 + self.assertEqual(result, graph.eval()) + + + +if __name__ == '__main__': + unittest.main() diff --git a/expression_graph/tests/test_node.py b/expression_graph/tests/test_node.py new file mode 100644 index 0000000..46b14b4 --- /dev/null +++ b/expression_graph/tests/test_node.py @@ -0,0 +1,48 @@ +#!/usr/bin/python3 + +from fractions import Fraction +from expression_graph.expression_node import ExpressionNode +from expression_graph.operator_node import OperatorNode +from expression_graph.value_node import ValueNode, IntegralValueNode, \ + RationalValueNode, RealValueNode +from expression_graph.operator import Operator + +import unittest + +class OperatorNodeTest(unittest.TestCase): + def testSimpleOperatorEvaluation(self): + lnode = IntegralValueNode(2) + rnode = IntegralValueNode(2) + opnode = OperatorNode(Operator('+'), lnode, rnode) + self.assertEqual('2 + 2', str(opnode)) + self.assertEqual(2, opnode.depth()) + self.assertEqual(4, opnode.eval()) + + +class IntegralNodeTest(unittest.TestCase): + def testSimpleNodeEvaluation(self): + node = IntegralValueNode(2) + self.assertEqual('2', str(node)) + self.assertEqual(1, node.depth()) + self.assertEqual(2, node.eval()) + + +class RationalNodeTest(unittest.TestCase): + def testSimpleNodeEvaluation(self): + node = RationalValueNode(Fraction(1, 2)) + self.assertEqual('1/2', str(node)) + self.assertEqual(1, node.depth()) + self.assertEqual(Fraction(1,2), node.eval()) + + +class RealNodeTest(unittest.TestCase): + def testSimpleNodeEvaluation(self): + node = RealValueNode(1.2) + self.assertEqual('1.2', str(node)) + self.assertEqual(1, node.depth()) + self.assertEqual(1.2, node.eval()) + + + +if __name__ == '__main__': + unittest.main() diff --git a/expression_graph/tests/test_operator.py b/expression_graph/tests/test_operator.py new file mode 100644 index 0000000..04d6d8e --- /dev/null +++ b/expression_graph/tests/test_operator.py @@ -0,0 +1,47 @@ +#!/usr/bin/python3 + +from fractions import Fraction +from expression_graph.operator import Operator, IntegralOperator, \ + RationalOperator, RealOperator, UnsupportedOperatorException + +import unittest + +class IntegralOperatorTest(unittest.TestCase): + def testSimpleApplication(self): + addop = IntegralOperator('+') + self.assertEqual(4, addop.apply(2, 2)) + + def testUnsupportedOperator(self): + self.assertRaises(UnsupportedOperatorException, + IntegralOperator, '**') + + def testDivisionPromoteToFraction(self): + divop = IntegralOperator('/') + quotient = divop.apply(4, 6) + self.assertEqual(type(quotient), Fraction) + self.assertEqual(Fraction(2, 3), quotient) + + +class RationalOperatorTest(unittest.TestCase): + def testSimpleApplication(self): + addop = RationalOperator('+') + self.assertEqual(1, addop.apply(Fraction(1, 2), Fraction(1, 2))) + + def testUnsupportedOperator(self): + self.assertRaises(UnsupportedOperatorException, + RationalOperator, '**') + + + +class RealOperatorTest(unittest.TestCase): + def testSimpleApplication(self): + addop = RealOperator('+') + self.assertEqual(4.0, addop.apply(2.0, 2.0)) + + def testUnsupportedOperator(self): + self.assertRaises(UnsupportedOperatorException, + RealOperator, '**') + + +if __name__ == '__main__': + unittest.main() diff --git a/expression_graph/tests/test_operator_node.py b/expression_graph/tests/test_operator_node.py new file mode 100644 index 0000000..dbe4749 --- /dev/null +++ b/expression_graph/tests/test_operator_node.py @@ -0,0 +1,16 @@ +#!/usr/bin/python3 + +from expression_graph.expression_node import ExpressionNode +from expression_graph.operator_node import OperatorNode, IntegralOperatorNode, \ + RationalOperatorNode, RealOperatorNode +from expression_graph.value_node import ValueNode, IntegralValueNode, \ + RationalValueNode, RealValueNode + +import unittest + + + + + +if __name__ == '__main__': + unittest.main() diff --git a/expression_graph/value_node.py b/expression_graph/value_node.py new file mode 100644 index 0000000..f6a1b35 --- /dev/null +++ b/expression_graph/value_node.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 + +from fractions import Fraction +from .expression_node import ExpressionNode + +class NodeValueTypeException(Exception): + pass + + +class ValueNode(ExpressionNode): + def __init__(self, value): + super().__init__(value) + + def eval(self): + return self._node + + def depth(self): + return 1 + +class IntegralValueNode(ValueNode): + def __init__(self, value): + if type(value) is not int: + raise NodeValueTypeException("{0} is not an integer".format(value)) + else: + super().__init__(value) + +class RationalValueNode(ValueNode): + def __init__(self, value): + if type(value) is not Fraction: + raise NodeValueTypeException("{0} is not a fraction".format(value)) + else: + super().__init__(value) + +class RealValueNode(ValueNode): + def __init__(self, value): + if type(value) is not float: + raise NodeValueTypeException("{0} is not a float".format(value)) + else: + super().__init__(value)