From 26f10ae0c250c989b6a6767f24cec91a76d812ba Mon Sep 17 00:00:00 2001 From: Jay Osako Date: Fri, 27 Mar 2015 20:36:48 -0400 Subject: [PATCH 1/9] Initial commit of a skeleton for the project. --- .gitignore | 4 ++ expression_graph/__init__.py | 2 + expression_graph/expression_graph.py | 1 + expression_graph/expression_node.py | 4 ++ expression_graph/operator.py | 57 +++++++++++++++++++ expression_graph/operator_node.py | 4 ++ .../tests/test_expression_graph.py | 6 ++ .../tests/test_expression_node.py | 13 +++++ expression_graph/tests/test_operator.py | 30 ++++++++++ expression_graph/value_node.py | 13 +++++ 10 files changed, 134 insertions(+) create mode 100644 .gitignore create mode 100644 expression_graph/__init__.py create mode 100644 expression_graph/expression_graph.py create mode 100644 expression_graph/expression_node.py create mode 100644 expression_graph/operator.py create mode 100644 expression_graph/operator_node.py create mode 100644 expression_graph/tests/test_expression_graph.py create mode 100644 expression_graph/tests/test_expression_node.py create mode 100644 expression_graph/tests/test_operator.py create mode 100644 expression_graph/value_node.py 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/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..d8d9549 --- /dev/null +++ b/expression_graph/expression_graph.py @@ -0,0 +1 @@ +#!/usr/bin/python3 diff --git a/expression_graph/expression_node.py b/expression_graph/expression_node.py new file mode 100644 index 0000000..6b802a5 --- /dev/null +++ b/expression_graph/expression_node.py @@ -0,0 +1,4 @@ +#!/usr/bin/python3 + +class ExpressionNode(Object); + pass diff --git a/expression_graph/operator.py b/expression_graph/operator.py new file mode 100644 index 0000000..dfa0417 --- /dev/null +++ b/expression_graph/operator.py @@ -0,0 +1,57 @@ +#!/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) + + +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..c6a4200 --- /dev/null +++ b/expression_graph/operator_node.py @@ -0,0 +1,4 @@ +#!/usr/bin/python3 + +class OperatorNode(ExpressionNode); + pass diff --git a/expression_graph/tests/test_expression_graph.py b/expression_graph/tests/test_expression_graph.py new file mode 100644 index 0000000..ad7869f --- /dev/null +++ b/expression_graph/tests/test_expression_graph.py @@ -0,0 +1,6 @@ +#!/usr/bin/python3 + +from unittest import TestCase + +class ExpressionGraphTest(TestCase); + pass diff --git a/expression_graph/tests/test_expression_node.py b/expression_graph/tests/test_expression_node.py new file mode 100644 index 0000000..4c1bcc3 --- /dev/null +++ b/expression_graph/tests/test_expression_node.py @@ -0,0 +1,13 @@ +#!/usr/bin/python3 + +from expression_graph import OperatorNode, IntegralOperatorNode, \ + RationalOperatorNode, RealOperatorNode, \ + ValueNode, IntegralValueNode, RationalValueNode, RealValueNode + +from unittest import TestCase + +class OperatorNodeTest(TestCase); + pass + +class OperatorNodeTest(TestCase); + pass diff --git a/expression_graph/tests/test_operator.py b/expression_graph/tests/test_operator.py new file mode 100644 index 0000000..e334caf --- /dev/null +++ b/expression_graph/tests/test_operator.py @@ -0,0 +1,30 @@ +#!/usr/bin/python3 +from fractions import Fraction +from expression_graph.operator import IntegralOperator, RationalOperator, \ + RealOperator + +import unittest + +class IntegralOperatorTest(unittest.TestCase): + def testSimpleApplication(self): + addop = IntegralOperator('+') + self.assertEqual(4, addop.apply(2, 2)) + + + +class RationalOperatorTest(unittest.TestCase): + def testSimpleApplication(self): + addop = RationalOperator('+') + self.assertEqual(1, addop.apply(Fraction(1, 2), Fraction(1, 2))) + + + +class RealOperatorTest(unittest.TestCase): + def testSimpleApplication(self): + addop = RealOperator('+') + self.assertEqual(4.0, addop.apply(2.0, 2.0)) + + + +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..cf49f39 --- /dev/null +++ b/expression_graph/value_node.py @@ -0,0 +1,13 @@ +#!/usr/bin/python3 + +class ValueNode(ExpressionNode); + pass + +class IntegralValueNode(ValueNode); + pass + +class RationalValueNode(ValueNode); + pass + +class RealValueNode(ValueNode); + pass From a113fb0e5d352784f2060a68f7e1ce70675a4bd3 Mon Sep 17 00:00:00 2001 From: Jay Osako Date: Sat, 28 Mar 2015 10:19:04 -0400 Subject: [PATCH 2/9] Added more tests for Operator classes. --- expression_graph/tests/test_operator.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/expression_graph/tests/test_operator.py b/expression_graph/tests/test_operator.py index e334caf..946d25b 100644 --- a/expression_graph/tests/test_operator.py +++ b/expression_graph/tests/test_operator.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 from fractions import Fraction -from expression_graph.operator import IntegralOperator, RationalOperator, \ - RealOperator +from expression_graph.operator import Operator, IntegralOperator, \ + RationalOperator, RealOperator, UnsupportedOperatorException import unittest @@ -10,20 +10,36 @@ 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__': From ea1f0ea79e6f2484a30f6315c32617404e61e688 Mon Sep 17 00:00:00 2001 From: Jay Osako Date: Sat, 28 Mar 2015 12:46:48 -0400 Subject: [PATCH 3/9] Temporary commit will trying to work out why test_node isn't being recongized by unittest. --- expression_graph/expression_graph.py | 8 ++- expression_graph/expression_node.py | 9 ++- expression_graph/operator.py | 4 ++ expression_graph/operator_node.py | 66 ++++++++++++++++++- .../tests/test_expression_graph.py | 2 +- .../tests/test_expression_node.py | 13 ---- expression_graph/tests/test_node.py | 25 +++++++ expression_graph/tests/test_operator.py | 1 + expression_graph/tests/test_operator_node.py | 16 +++++ expression_graph/value_node.py | 37 +++++++++-- 10 files changed, 155 insertions(+), 26 deletions(-) delete mode 100644 expression_graph/tests/test_expression_node.py create mode 100644 expression_graph/tests/test_node.py create mode 100644 expression_graph/tests/test_operator_node.py diff --git a/expression_graph/expression_graph.py b/expression_graph/expression_graph.py index d8d9549..f1a7be2 100644 --- a/expression_graph/expression_graph.py +++ b/expression_graph/expression_graph.py @@ -1 +1,7 @@ -#!/usr/bin/python3 +#!/usr/bin/python3 + +from .expression_node import ExpressionNode + +class ExpressionGraph(Object); + + def __init__(self, root_node): diff --git a/expression_graph/expression_node.py b/expression_graph/expression_node.py index 6b802a5..ade08d7 100644 --- a/expression_graph/expression_node.py +++ b/expression_graph/expression_node.py @@ -1,4 +1,9 @@ #!/usr/bin/python3 -class ExpressionNode(Object); - pass +class ExpressionNode(object): + + def __init__(self): + pass + + def eval(self): + pass diff --git a/expression_graph/operator.py b/expression_graph/operator.py index dfa0417..fc9e49e 100644 --- a/expression_graph/operator.py +++ b/expression_graph/operator.py @@ -24,7 +24,11 @@ def __init__(self, operator): def apply(self, operand, operahend): return self.operation.get(self._operator)(operand, operahend) + def symbol(self): + return self._operator + + class IntegralOperator(Operator): operation = {Operator.ADD: int.__add__, Operator.SUBTRACT: int.__sub__, diff --git a/expression_graph/operator_node.py b/expression_graph/operator_node.py index c6a4200..1a9f78c 100644 --- a/expression_graph/operator_node.py +++ b/expression_graph/operator_node.py @@ -1,4 +1,66 @@ #!/usr/bin/python3 - -class OperatorNode(ExpressionNode); + +from .operator import Operator +from .expression_node import ExpressionNode +from .value_node import ValueNode, IntegralNode, RationalNode, RealNode + + +class InvalidOperatorException(Exception): pass + +class InvalidValueException(Exception): + pass + +class OperatorNode(ExpressionNode): + nodeTypes = [RealNode, RationalNode, IntegralNode] + + def __init__(self, operator, lnode, rnode): + if not isinstance(Operator, operator): + raise InvalidOperatorException('{0} is not a supported operator'.format(operator)) + elif not isinstance(ExpressionNode, lval): + raise InvalidValueException('{0} is not a valid node'.format(lnode)) + elif not isinstance(ExpressionNode, rval): + raise InvalidValueException('{0} is not a valid node'.format(rnode)) + else: + self._operator = operator + self._lnode = lnode + self._rnode = rnode + + def eval(self): + lvalue = self._lnode.eval() + rvalue = self._rnode.eval() + lnode_type = type(self._lnode) + rnode_type = type(self._rnode) + + effective_operator = self._operator + 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 lnode_type == RealNode: + effective_operator = RealOperator(self.__operator.symbol()) + if rnode_type == RationalNode: + effective_rvalue = rvalue.numerator() // rvalue.denominator() + elif rnode_type == IntegralNode: + effective_rvalue = float(rvalue) + else: + effective_rvalue = rvalue + elif lnode_type == RationalNode: + if rnode_type == RealNode: + effective_operator = RealOperator(self.__operator.symbol()) + effective_lvalue = lvalue.numerator() // lvalue.denominator() + else: + effective_operator = RationalOperator(self.__operator.symbol()) + else: + if rnode_type == RealNode: + effective_operator = RealOperator(self.__operator.symbol()) + effective_lvalue = float(lvalue) + elif rnode_type == RationalNode: + effective_operator = RationalOperator(self.__operator.symbol()) + else: + effective_operator = IntegralOperator(self.__operator.symbol()) + + return effective_operator.apply(effective_lvalue, effective_rvalue) + diff --git a/expression_graph/tests/test_expression_graph.py b/expression_graph/tests/test_expression_graph.py index ad7869f..3d5ae3e 100644 --- a/expression_graph/tests/test_expression_graph.py +++ b/expression_graph/tests/test_expression_graph.py @@ -2,5 +2,5 @@ from unittest import TestCase -class ExpressionGraphTest(TestCase); +class ExpressionGraphTest(TestCase): pass diff --git a/expression_graph/tests/test_expression_node.py b/expression_graph/tests/test_expression_node.py deleted file mode 100644 index 4c1bcc3..0000000 --- a/expression_graph/tests/test_expression_node.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/python3 - -from expression_graph import OperatorNode, IntegralOperatorNode, \ - RationalOperatorNode, RealOperatorNode, \ - ValueNode, IntegralValueNode, RationalValueNode, RealValueNode - -from unittest import TestCase - -class OperatorNodeTest(TestCase); - pass - -class OperatorNodeTest(TestCase); - pass diff --git a/expression_graph/tests/test_node.py b/expression_graph/tests/test_node.py new file mode 100644 index 0000000..db9ad94 --- /dev/null +++ b/expression_graph/tests/test_node.py @@ -0,0 +1,25 @@ +#!/usr/bin/python3 + +from fractions import Fraction +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 + +class OperatorNodeTest(unittest.TestCase): + def testSimpleOperatorEvaluation(self): + lnode = IntegralNode(2) + rnode = IntegralNode(2) + opnode = OperatorNode(Operator('+'), lnode, rnode) + self.assertEqual(4, opnode.eval()) + +class IntegralNodeTest(unittest.TestCase): + def testSimpleNodeEvaluation(self): + node = IntegralNode(2) + self.assertEqual(2, node.eval()) + +if __name__ == '__main__': + unittest.main() diff --git a/expression_graph/tests/test_operator.py b/expression_graph/tests/test_operator.py index 946d25b..04d6d8e 100644 --- a/expression_graph/tests/test_operator.py +++ b/expression_graph/tests/test_operator.py @@ -1,4 +1,5 @@ #!/usr/bin/python3 + from fractions import Fraction from expression_graph.operator import Operator, IntegralOperator, \ RationalOperator, RealOperator, UnsupportedOperatorException 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 index cf49f39..0a1b837 100644 --- a/expression_graph/value_node.py +++ b/expression_graph/value_node.py @@ -1,13 +1,36 @@ #!/usr/bin/python3 -class ValueNode(ExpressionNode); - pass +from .expression_node import ExpressionNode -class IntegralValueNode(ValueNode); +class NodeValueTypeException(Exception): pass -class RationalValueNode(ValueNode); - pass -class RealValueNode(ValueNode); - pass +class ValueNode(ExpressionNode): + def __init__(self, value): + self._value = value + + def eval(self): + return self._value + + +class IntegralValueNode(ValueNode): + def __init__(self, value): + if type(value) is not int: + raise NodeValueTypeException("{0} is not an integer".format(value)) + else: + self.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: + self.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: + self.super().__init(value) From c155bb243822808c25f23d4756e2556fef6dc9c9 Mon Sep 17 00:00:00 2001 From: Jay Osako Date: Sat, 28 Mar 2015 13:02:05 -0400 Subject: [PATCH 4/9] Fixed testing issue, corrected issues with operator_node.py --- expression_graph/operator_node.py | 35 +++++++++++++++-------------- expression_graph/tests/test_node.py | 10 ++++----- expression_graph/value_node.py | 6 ++--- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/expression_graph/operator_node.py b/expression_graph/operator_node.py index 1a9f78c..d7f2f22 100644 --- a/expression_graph/operator_node.py +++ b/expression_graph/operator_node.py @@ -1,8 +1,9 @@ #!/usr/bin/python3 -from .operator import Operator +from .operator import Operator, IntegralOperator, RationalOperator, RealOperator from .expression_node import ExpressionNode -from .value_node import ValueNode, IntegralNode, RationalNode, RealNode +from .value_node import ValueNode, IntegralValueNode, RationalValueNode, \ + RealValueNode class InvalidOperatorException(Exception): @@ -12,14 +13,14 @@ class InvalidValueException(Exception): pass class OperatorNode(ExpressionNode): - nodeTypes = [RealNode, RationalNode, IntegralNode] + nodeTypes = [RealValueNode, RationalValueNode, IntegralValueNode] def __init__(self, operator, lnode, rnode): - if not isinstance(Operator, operator): + if not isinstance(operator, Operator): raise InvalidOperatorException('{0} is not a supported operator'.format(operator)) - elif not isinstance(ExpressionNode, lval): + elif not isinstance(lnode, ExpressionNode): raise InvalidValueException('{0} is not a valid node'.format(lnode)) - elif not isinstance(ExpressionNode, rval): + elif not isinstance(rnode, ExpressionNode): raise InvalidValueException('{0} is not a valid node'.format(rnode)) else: self._operator = operator @@ -39,28 +40,28 @@ def eval(self): # 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 lnode_type == RealNode: + if lnode_type == RealValueNode: effective_operator = RealOperator(self.__operator.symbol()) - if rnode_type == RationalNode: + if rnode_type == RationalValueNode: effective_rvalue = rvalue.numerator() // rvalue.denominator() - elif rnode_type == IntegralNode: + elif rnode_type == IntegralValueNode: effective_rvalue = float(rvalue) else: effective_rvalue = rvalue - elif lnode_type == RationalNode: - if rnode_type == RealNode: + elif lnode_type == RationalValueNode: + if rnode_type == RealValueNode: effective_operator = RealOperator(self.__operator.symbol()) effective_lvalue = lvalue.numerator() // lvalue.denominator() else: - effective_operator = RationalOperator(self.__operator.symbol()) + effective_operator = RationalOperator(self._operator.symbol()) else: - if rnode_type == RealNode: - effective_operator = RealOperator(self.__operator.symbol()) + if rnode_type == RealValueNode: + effective_operator = RealOperator(self._operator.symbol()) effective_lvalue = float(lvalue) - elif rnode_type == RationalNode: - effective_operator = RationalOperator(self.__operator.symbol()) + elif rnode_type == RationalValueNode: + effective_operator = RationalOperator(self._operator.symbol()) else: - effective_operator = IntegralOperator(self.__operator.symbol()) + effective_operator = IntegralOperator(self._operator.symbol()) return effective_operator.apply(effective_lvalue, effective_rvalue) diff --git a/expression_graph/tests/test_node.py b/expression_graph/tests/test_node.py index db9ad94..2beebb6 100644 --- a/expression_graph/tests/test_node.py +++ b/expression_graph/tests/test_node.py @@ -2,23 +2,23 @@ from fractions import Fraction from expression_graph.expression_node import ExpressionNode -from expression_graph.operator_node import OperatorNode, IntegralOperatorNode, \ - RationalOperatorNode, RealOperatorNode +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 = IntegralNode(2) - rnode = IntegralNode(2) + lnode = IntegralValueNode(2) + rnode = IntegralValueNode(2) opnode = OperatorNode(Operator('+'), lnode, rnode) self.assertEqual(4, opnode.eval()) class IntegralNodeTest(unittest.TestCase): def testSimpleNodeEvaluation(self): - node = IntegralNode(2) + node = IntegralValueNode(2) self.assertEqual(2, node.eval()) if __name__ == '__main__': diff --git a/expression_graph/value_node.py b/expression_graph/value_node.py index 0a1b837..0cceba2 100644 --- a/expression_graph/value_node.py +++ b/expression_graph/value_node.py @@ -19,18 +19,18 @@ def __init__(self, value): if type(value) is not int: raise NodeValueTypeException("{0} is not an integer".format(value)) else: - self.super().__init(value) + 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: - self.super().__init(value) + 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: - self.super().__init(value) + super().__init__(value) From 1db895bfe5e69c0bd2ce1979d2132e09bbf57387 Mon Sep 17 00:00:00 2001 From: Jay Osako Date: Sun, 29 Mar 2015 17:04:50 -0400 Subject: [PATCH 5/9] Checkpointing at partial completion of expression_graph. --- expression_graph/expression_graph.py | 142 +++++++++++++++++- expression_graph/expression_node.py | 7 +- expression_graph/operator.py | 2 +- expression_graph/operator_node.py | 66 +++++--- .../tests/test_expression_graph.py | 33 +++- expression_graph/tests/test_node.py | 23 +++ expression_graph/value_node.py | 7 +- 7 files changed, 250 insertions(+), 30 deletions(-) diff --git a/expression_graph/expression_graph.py b/expression_graph/expression_graph.py index f1a7be2..f5fdd23 100644 --- a/expression_graph/expression_graph.py +++ b/expression_graph/expression_graph.py @@ -1,7 +1,145 @@ #!/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 ExpressionGraph(Object); +class InvalidGraphException(Exception): + pass - def __init__(self, root_node): +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 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 (head is None and len(elements) == 0): + raise EmptyExpressionException() + + 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) + + try: + next_element.iterate() + remainder_start = next_element.value() + remaining_elements = elements[remainder_start:] + super().__init__(LeftmostEvaluatingExpressionGraph(*remaining_elements, head=self._node)) + except IncompleteExpressionException as e: + 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.""" + print(self) + if 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 index ade08d7..2602b01 100644 --- a/expression_graph/expression_node.py +++ b/expression_graph/expression_node.py @@ -2,8 +2,11 @@ class ExpressionNode(object): - def __init__(self): - pass + 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 index fc9e49e..3926c56 100644 --- a/expression_graph/operator.py +++ b/expression_graph/operator.py @@ -24,7 +24,7 @@ def __init__(self, operator): def apply(self, operand, operahend): return self.operation.get(self._operator)(operand, operahend) - def symbol(self): + def __str__(self): return self._operator diff --git a/expression_graph/operator_node.py b/expression_graph/operator_node.py index d7f2f22..16370bd 100644 --- a/expression_graph/operator_node.py +++ b/expression_graph/operator_node.py @@ -1,10 +1,13 @@ #!/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 @@ -12,6 +15,7 @@ class InvalidOperatorException(Exception): class InvalidValueException(Exception): pass + class OperatorNode(ExpressionNode): nodeTypes = [RealValueNode, RationalValueNode, IntegralValueNode] @@ -23,45 +27,63 @@ def __init__(self, operator, lnode, rnode): elif not isinstance(rnode, ExpressionNode): raise InvalidValueException('{0} is not a valid node'.format(rnode)) else: - self._operator = operator + super().__init__(operator) self._lnode = lnode self._rnode = rnode def eval(self): + opsym = str(self._node) lvalue = self._lnode.eval() rvalue = self._rnode.eval() - lnode_type = type(self._lnode) - rnode_type = type(self._rnode) + lvalue_type = type(lvalue) + rvalue_type = type(rvalue) - effective_operator = self._operator + 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. + # 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 lnode_type == RealValueNode: - effective_operator = RealOperator(self.__operator.symbol()) - if rnode_type == RationalValueNode: + if lvalue_type == float: + effective_operator = RealOperator(opsym) + if rvalue_type == Fraction: effective_rvalue = rvalue.numerator() // rvalue.denominator() - elif rnode_type == IntegralValueNode: + elif rvalue_type == int: effective_rvalue = float(rvalue) + elif rvalue_type == float: + pass else: - effective_rvalue = rvalue - elif lnode_type == RationalValueNode: - if rnode_type == RealValueNode: - effective_operator = RealOperator(self.__operator.symbol()) + raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(lnode_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: - effective_operator = RationalOperator(self._operator.symbol()) - else: - if rnode_type == RealValueNode: - effective_operator = RealOperator(self._operator.symbol()) + raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(lnode_type)) + + elif lvalue_type == int: + if rvalue_type == float: + effective_operator = RealOperator(opsym) effective_lvalue = float(lvalue) - elif rnode_type == RationalValueNode: - effective_operator = RationalOperator(self._operator.symbol()) + elif rvalue_type == Fraction: + effective_operator = RationalOperator(opsym) + elif rvalue_type == int: + effective_operator = IntegralOperator(opsym) else: - effective_operator = IntegralOperator(self._operator.symbol()) - + raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(rnode_type)) + else: + raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(lnode_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 index 3d5ae3e..33a60a1 100644 --- a/expression_graph/tests/test_expression_graph.py +++ b/expression_graph/tests/test_expression_graph.py @@ -1,6 +1,37 @@ #!/usr/bin/python3 +from fractions import Fraction +from ..expression_graph import ExpressionGraph, \ + LeftmostEvaluatingExpressionGraph, EmptyExpressionException, \ + IncompleteExpressionException + from unittest import TestCase class ExpressionGraphTest(TestCase): - pass + 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 testExtendedExpression(self): + graph = LeftmostEvaluatingExpressionGraph(2, '+', 2, '-', 6, '*', 4, '/', 2) + self.assertEqual('2 + 2 - 6 * 4 / 2', str(graph)) + self.assertEqual(-4, graph.eval()) diff --git a/expression_graph/tests/test_node.py b/expression_graph/tests/test_node.py index 2beebb6..46b14b4 100644 --- a/expression_graph/tests/test_node.py +++ b/expression_graph/tests/test_node.py @@ -14,12 +14,35 @@ 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/value_node.py b/expression_graph/value_node.py index 0cceba2..f6a1b35 100644 --- a/expression_graph/value_node.py +++ b/expression_graph/value_node.py @@ -1,5 +1,6 @@ #!/usr/bin/python3 +from fractions import Fraction from .expression_node import ExpressionNode class NodeValueTypeException(Exception): @@ -8,11 +9,13 @@ class NodeValueTypeException(Exception): class ValueNode(ExpressionNode): def __init__(self, value): - self._value = value + super().__init__(value) def eval(self): - return self._value + return self._node + def depth(self): + return 1 class IntegralValueNode(ValueNode): def __init__(self, value): From 8849eb204b5d880c8af14386f3ea926991eba776 Mon Sep 17 00:00:00 2001 From: Jay Osako Date: Sun, 29 Mar 2015 17:34:27 -0400 Subject: [PATCH 6/9] Completed working LeftmostEvaluatingExpressionGraph and basic tests. --- expression_graph/expression_graph.py | 21 ++++++++++++------- .../tests/test_expression_graph.py | 5 +++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/expression_graph/expression_graph.py b/expression_graph/expression_graph.py index f5fdd23..88fb5d9 100644 --- a/expression_graph/expression_graph.py +++ b/expression_graph/expression_graph.py @@ -33,7 +33,9 @@ def depth(self): return self._graph._node.depth() def __str__(self): - if isinstance(self._graph, ExpressionNode): + if self._graph is None: + return '' + elif isinstance(self._graph, ExpressionNode): return str(self._graph) else: return str(self._graph._node) @@ -89,26 +91,29 @@ def __init__(self, *elements, **kwargs): rnode = assignValueNodeType(rvalue) self._node = OperatorNode(Operator(opvalue), lnode, rnode) - try: next_element.iterate() remainder_start = next_element.value() remaining_elements = elements[remainder_start:] - super().__init__(LeftmostEvaluatingExpressionGraph(*remaining_elements, head=self._node)) + self._node = LeftmostEvaluatingExpressionGraph(*remaining_elements, head=self._node) except IncompleteExpressionException as e: + pass + finally: 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.""" - print(self) - if isinstance(self._graph, ExpressionNode): + 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) diff --git a/expression_graph/tests/test_expression_graph.py b/expression_graph/tests/test_expression_graph.py index 33a60a1..7313ffe 100644 --- a/expression_graph/tests/test_expression_graph.py +++ b/expression_graph/tests/test_expression_graph.py @@ -31,6 +31,11 @@ def testDivisionExpression(self): 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)) From c5ce2a7e42f1bf4e3c6d9bbf3812eb5923af4806 Mon Sep 17 00:00:00 2001 From: Jay Osako Date: Sun, 29 Mar 2015 18:15:36 -0400 Subject: [PATCH 7/9] Added test for handling of incomplete expressions, corrected some issues highlighted by same. --- expression_graph/expression_graph.py | 7 +++++-- expression_graph/operator_node.py | 9 +++++---- expression_graph/tests/test_expression_graph.py | 3 +++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/expression_graph/expression_graph.py b/expression_graph/expression_graph.py index 88fb5d9..8f73df0 100644 --- a/expression_graph/expression_graph.py +++ b/expression_graph/expression_graph.py @@ -50,6 +50,8 @@ def __init__(self, *elements, **kwargs): if elements is None or (head is None and len(elements) == 0): 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 @@ -86,19 +88,20 @@ def __init__(self, *elements, **kwargs): 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:] - self._node = LeftmostEvaluatingExpressionGraph(*remaining_elements, head=self._node) 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): diff --git a/expression_graph/operator_node.py b/expression_graph/operator_node.py index 16370bd..e5e159d 100644 --- a/expression_graph/operator_node.py +++ b/expression_graph/operator_node.py @@ -23,6 +23,7 @@ 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)) @@ -54,7 +55,7 @@ def eval(self): elif rvalue_type == float: pass else: - raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(lnode_type)) + raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(lvalue_type)) elif lvalue_type == Fraction: if rvalue_type == float: @@ -63,7 +64,7 @@ def eval(self): elif rvalue_type in [int, Fraction]: effective_operator = RationalOperator(opsym) else: - raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(lnode_type)) + raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(lvalue_type)) elif lvalue_type == int: if rvalue_type == float: @@ -74,9 +75,9 @@ def eval(self): elif rvalue_type == int: effective_operator = IntegralOperator(opsym) else: - raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(rnode_type)) + raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(rvalue_type)) else: - raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(lnode_type)) + raise UnsupportedNodeTypeException('{0} is not a supported node type'.format(lvalue_type)) return effective_operator.apply(effective_lvalue, effective_rvalue) diff --git a/expression_graph/tests/test_expression_graph.py b/expression_graph/tests/test_expression_graph.py index 7313ffe..7e20c57 100644 --- a/expression_graph/tests/test_expression_graph.py +++ b/expression_graph/tests/test_expression_graph.py @@ -40,3 +40,6 @@ 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, '/') From 49463b3867b85981838d49cd4f15d10ef3fb2093 Mon Sep 17 00:00:00 2001 From: Jay Osako Date: Sun, 29 Mar 2015 18:25:00 -0400 Subject: [PATCH 8/9] Added tests for fractional and real results. --- .../tests/test_expression_graph.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/expression_graph/tests/test_expression_graph.py b/expression_graph/tests/test_expression_graph.py index 7e20c57..0de0fa0 100644 --- a/expression_graph/tests/test_expression_graph.py +++ b/expression_graph/tests/test_expression_graph.py @@ -43,3 +43,22 @@ def testExtendedExpression(self): 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()) + + + + + + From 58fcac8cc459d38ff8b74cdf86fdb3822581751e Mon Sep 17 00:00:00 2001 From: Jay Osako Date: Sun, 29 Mar 2015 19:09:18 -0400 Subject: [PATCH 9/9] Added simple test parser for project. --- SimpleExpressionEvaluator.py | 49 +++++++++++++++++++ expression_graph/expression_graph.py | 2 +- .../tests/test_expression_graph.py | 5 +- 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100755 SimpleExpressionEvaluator.py 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/expression_graph.py b/expression_graph/expression_graph.py index 8f73df0..1ba20e5 100644 --- a/expression_graph/expression_graph.py +++ b/expression_graph/expression_graph.py @@ -48,7 +48,7 @@ def __init__(self, *elements, **kwargs): if 'head' in kwargs.keys(): head = kwargs['head'] - if elements is None or (head is None and len(elements) == 0): + 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() diff --git a/expression_graph/tests/test_expression_graph.py b/expression_graph/tests/test_expression_graph.py index 0de0fa0..5e1aff5 100644 --- a/expression_graph/tests/test_expression_graph.py +++ b/expression_graph/tests/test_expression_graph.py @@ -59,6 +59,5 @@ def testExtendedWithRealResultExpression(self): - - - +if __name__ == '__main__': + unittest.main()