Просмотр исходного кода

Implemented node comparison and normalisation.

 - Node comparison using the __lt__ operator is implemented.
 - Node normalisation is implemented in extract_polynome_properties.
 - Enabled the remaining polynome test cases (all unit tests succeed).
 - Comma can be used to generate multiple expression leafs and nodes at once.
Sander Mathijs van Veen 14 лет назад
Родитель
Сommit
b39746223f
6 измененных файлов с 185 добавлено и 113 удалено
  1. 80 17
      src/node.py
  2. 28 18
      src/parser.py
  3. 2 2
      src/rules/__init__.py
  4. 24 25
      src/rules/poly.py
  5. 14 10
      tests/test_node.py
  6. 37 41
      tests/test_rules_poly.py

+ 80 - 17
src/node.py

@@ -29,6 +29,7 @@ OP_MOD = 7
 # N-ary (functions)
 OP_INT = 8
 OP_EXPAND = 9
+OP_COMMA = 10
 
 
 TYPE_MAP = {
@@ -47,10 +48,50 @@ OP_MAP = {
         'mod': OP_MOD,
         'int': OP_INT,
         'expand': OP_EXPAND,
+        ',': OP_COMMA,
         }
 
 
 class ExpressionBase(object):
+    def __lt__(self, other):
+        """
+        Comparison between this expression{node,leaf} and another
+        expression{node,leaf}. This comparison will return True if this
+        instance has less value than the other expression{node,leaf}.
+        Otherwise, False is returned.
+
+        The comparison is based on the following conditions:
+
+        1. Both are leafs. String comparison of the value is used.
+        2. This is a leaf and other is a node. This leaf has less value, thus
+           True is returned.
+        3. This is a node and other is a leaf. This leaf has more value, thus
+           False is returned.
+        4. Both are nodes. Compare the polynome properties of the nodes. True
+           is returned if this node's root property is less than other's root
+           property, or this node's exponent property is less than other's
+           exponent property, or this node's coefficient property is less than
+           other's coefficient property. Otherwise, False is returned.
+        """
+        if self.is_leaf():
+            if other.is_leaf():
+                # Both are leafs, string compare the value.
+                return str(self.value) < str(other.value)
+            # Self is a leaf, thus has less value than an expression node.
+            return True
+
+        if other.is_leaf():
+            # Self is an expression node, and the other is a leaf. Thus, other
+            # is greater than self.
+            return False
+
+        # Both are nodes, compare the polynome properties.
+        s_coeff, s_root, s_exp = self.extract_polynome_properties()
+        o_coeff, o_root, o_exp = other.extract_polynome_properties()
+
+        return s_root < o_root or s_exp < or_exp or s_coeff < or_coeff
+
+
     def is_leaf(self):
         return self.type != TYPE_OPERATOR
 
@@ -94,26 +135,38 @@ class ExpressionNode(Node, ExpressionBase):
         node.parent = self.parent
         self.parent = None
 
-    def get_polynome(self):
+    def extract_polynome_properties(self):
         """
-        Identifier nodes of all polynomes, tuple format is:
-        (root, exponent, coefficient, literal_exponent)
+        Extract polynome properties into tuple format: (coefficient, root,
+        exponent). Thus: c * r ^ e will be extracted into the tuple (c, r, e).
+
+        This function will normalize the expression before extracting the
+        properties. Therefore, the expression r ^ e * c results the same tuple
+        (c, r, e) as the expression c * r ^ e.
+
+
+        >>> from src.node import ExpressionNode as N, ExpressionLeaf as L
+        >>> c, r, e = L('c'), L('r'), L('e')
+        >>> n1 = N('*', c, N('^', r, e))
+        >>> n1.extract_polynome()
+        (c, r, e)
+        >>> n2 = N('*', N('^', r, e), c)
+        >>> n2.extract_polynome()
+        (c, r, e)
         """
         # TODO: change "get_polynome" -> "extract_polynome".
         # TODO: change retval of c * r ^ e to (c, r, e).
+        # was: (root, exponent, coefficient, literal_exponent)
 
-        # TODO: normalize c * r and r * c -> c * r. Otherwise, the tuple will
-        # not match if the order of the expression is different. Example:
-        #   r ^ e * c == c * r ^ e
-        # without normalization, those expressions will not match.
-
-        # rule: r ^ e
+        # rule: r ^ e -> (1, r, e)
         if self.is_power():
-            return (self[0], self[1], ExpressionLeaf(1), True)
+            return (ExpressionLeaf(1), self[0], self[1])
 
         if self.op != OP_MUL:
             return
 
+        # rule: 3 * 7 ^ e | 'a' * 'b' ^ e
+
         # expression: c * r ^ e ; tree:
         #
         #    *
@@ -125,16 +178,25 @@ class ExpressionNode(Node, ExpressionBase):
         # rule: c * r ^ e | (r ^ e) * c
         for i, j in [(0, 1), (1, 0)]:
             if self[j].is_power():
-                return (self[j][0], self[j][1], self[i], True)
+                return (self[i], self[j][0], self[j][1])
 
+        # Normalize c * r and r * c -> c * r. Otherwise, the tuple will not
+        # match if the order of the expression is different. Example:
+        #   r ^ e * c == c * r ^ e
+        # without normalization, those expressions will not match.
+        #
         # rule: c * r | r * c
-        return (self[0], ExpressionLeaf(1), self[1], False)
+        if self[0] < self[1]:
+            return (self[0], self[1], ExpressionLeaf(1))
+        return (self[1], self[0], ExpressionLeaf(1))
 
     def get_scope(self):
         """"""
         scope = []
         #op = OP_ADD | OP_SUB if self.op & (OP_ADD | OP_SUB) else self.op
 
+        # TODO: what to do with OP_SUB and OP_ADD in get_scope?
+
         for child in self:
             if not child.is_leaf() and child.op == self.op:
                 scope += child.get_scope()
@@ -150,13 +212,14 @@ class ExpressionLeaf(Leaf, ExpressionBase):
 
         self.type = TYPE_MAP[type(args[0])]
 
-    def get_polynome(self):
+    def extract_polynome_properties(self):
         """
-        Identifier nodes of all polynomes, tuple format is:
-        (identifier, exponent, coefficient, literal_exponent)
+        An expression leaf will return the polynome tuple (1, r, 1), where r is
+        the leaf itself. See also the method extract_polynome_properties in
+        ExpressionBase.
         """
-        # a = 1 * a ^ 1
-        return (self, ExpressionLeaf(1), ExpressionLeaf(1), False)
+        # rule: 1 * r ^ 1 -> (1, r, 1)
+        return (ExpressionLeaf(1), self, ExpressionLeaf(1))
 
     def replace(self, node):
         if not hasattr(self, 'parent'):

+ 28 - 18
src/parser.py

@@ -19,24 +19,24 @@ sys.path.insert(1, EXTERNAL_MODS)
 from pybison import BisonParser, BisonSyntaxError
 from graph_drawing.graph import generate_graph
 
-from node import TYPE_OPERATOR
+from node import TYPE_OPERATOR, OP_COMMA
 from rules import RULES
 from possibilities import filter_duplicates
 
 
-## Check for n-ary operator in child nodes
-#def combine(op, op_type, *nodes):
-#    # At least return the operator.
-#    res = [op]
-#
-#    for n in nodes:
-#        # Merge the children for all nodes which have the same operator.
-#        if n.type == TYPE_OPERATOR and n.op == op_type:
-#            res += n.nodes
-#        else:
-#            res.append(n)
-#
-#    return res
+# Check for n-ary operator in child nodes
+def combine(op, op_type, *nodes):
+    # At least return the operator.
+    res = [op]
+
+    for n in nodes:
+        # Merge the children for all nodes which have the same operator.
+        if n.type == TYPE_OPERATOR and n.op == op_type:
+            res += n.nodes
+        else:
+            res.append(n)
+
+    return res
 
 
 class Parser(BisonParser):
@@ -62,6 +62,7 @@ class Parser(BisonParser):
     # precedences
     # ------------------------------
     precedences = (
+        ('left', ('COMMA', )),
         ('left', ('MINUS', 'PLUS')),
         ('left', ('TIMES', 'DIVIDE')),
         ('left', ('NEG', )),
@@ -228,6 +229,7 @@ class Parser(BisonParser):
             | LPAREN exp RPAREN
             | unary
             | binary
+            | nary
         """
         #    | concat
 
@@ -242,12 +244,9 @@ class Parser(BisonParser):
         if option == 2:  # rule: LPAREN exp RPAREN
             return values[1]
 
-        if option in [3, 4]:  # rule: unary | binary
+        if option in [3, 4, 5]:  # rule: unary | binary | nary
             return values[0]
 
-        #if option in [3, 4, 5]:  # rule: unary | binary | concat
-        #    return values[0]
-
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
                                % (option, target))  # pragma: nocover
 
@@ -277,6 +276,17 @@ class Parser(BisonParser):
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
                                % (option, target))  # pragma: nocover
 
+    def on_nary(self, target, option, names, values):
+        """
+        nary : exp COMMA exp
+        """
+
+        if option == 0:  # rule: exp COMMA exp
+            return Node(*combine(',', OP_COMMA, values[0], values[2]))
+
+        raise BisonSyntaxError('Unsupported option %d in target "%s".'
+                               % (option, target))  # pragma: nocover
+
     #def on_concat(self, option, target, names, values):
     #    """
     #    concat : exp IDENTIFIER %prec TIMES

+ 2 - 2
src/rules/__init__.py

@@ -1,8 +1,8 @@
 from ..node import OP_ADD, OP_MUL
-from .poly import match_combine_factors, match_expand
+from .poly import match_combine_polynomes, match_expand
 
 
 RULES = {
-        OP_ADD: [match_combine_factors],
+        OP_ADD: [match_combine_polynomes],
         OP_MUL: [match_expand],
         }

+ 24 - 25
src/rules/poly.py

@@ -1,7 +1,6 @@
 from itertools import combinations
 
-from ..node import ExpressionNode as Node, ExpressionLeaf as Leaf, \
-        TYPE_OPERATOR, OP_ADD, OP_MUL
+from ..node import ExpressionNode as Node, TYPE_OPERATOR, OP_ADD, OP_MUL
 from ..possibilities import Possibility as P
 from .utils import nary_node
 
@@ -55,7 +54,7 @@ def expand_single(root, args):
     return nary_node('*', scope)
 
 
-def match_combine_factors(node):
+def match_combine_polynomes(node, verbose=False):
     """
     n + exp + m -> exp + (n + m)
     k0 * v ^ n + exp + k1 * v ^ n -> exp + (k0 + k1) * v ^ n
@@ -75,11 +74,14 @@ def match_combine_factors(node):
     #   (root, exponent, coefficient, literal_coefficient)
     polys = []
 
-    print 'match combine factors:', node
+    if verbose:
+        print 'match combine factors:', node
 
     for n in node.get_scope():
-        polynome = n.get_polynome()
-        print 'n:', n, 'polynome:', polynome
+        polynome = n.extract_polynome_properties()
+
+        if verbose:
+            print 'n:', n, 'polynome:', polynome
 
         if polynome:
             polys.append((n, polynome))
@@ -87,14 +89,13 @@ def match_combine_factors(node):
     # Each combination of powers of the same value and polynome can be added
     if len(polys) >= 2:
         for left, right in combinations(polys, 2):
-            r0, e0, c0 = left[1][:3]
-            r1, e1, c1 = right[1][:3]
-
-            if (r0.is_numeric() and r1.is_numeric() and e0 == e1 == Leaf(1)) \
-                    or (r0 == r1 and e0 == e1):
-                # Both numeric and same exponent -> combine coefficients and
-                # roots, or:
-                # Same identifier and exponent -> combine coefficients
+            c0, r0, e0 = left[1]
+            c1, r1, e1 = right[1]
+
+            # Both numeric root and same exponent -> combine coefficients and
+            # roots, or: same root and exponent -> combine coefficients.
+            if ((r0.is_numeric() and r1.is_numeric()) or r0 == r1) \
+                    and e0 == e1:
                 p.append(P(node, combine_polynomes, (left, right)))
 
     return p
@@ -102,7 +103,7 @@ def match_combine_factors(node):
 
 def combine_polynomes(root, args):
     """
-    Combine two identifier multiplications of any polynome in an n-ary plus.
+    Combine two multiplications of any polynome in an n-ary plus.
 
     Example:
     c * a ^ b + d * a ^ b -> (c + d) * a ^ b
@@ -113,17 +114,13 @@ def combine_polynomes(root, args):
 
     # TODO: verify that the two commented expression below are invalid and the
     # following two expressions are right.
-    r0, e0, c0 = pl
-    r1, e1, c1 = pr
-    #c0, r0, e0 = pl
-    #c1, r1, e1 = pr
+    c0, r0, e0 = pl
+    c1, r1, e1 = pr
 
-    scope = root.get_scope()
-
-    if r0.is_numeric() and r1.is_numeric() and e0 == e1 == 1:
-        new_root = Leaf(r0.value + r1.value)
-    else:
-        new_root = r0
+    #if r0.is_numeric() and r1.is_numeric() and e0 == e1 == 1:
+    #    new_root = Leaf(r0.value + r1.value)
+    #else:
+    #    new_root = r0
 
     if pl[3] or pr[3]:
         # literal a ^ 1 -> a ^ 1
@@ -136,6 +133,8 @@ def combine_polynomes(root, args):
     # a, b and c are from 'left', d is from 'right'.
     replacement = Node('*', Node('+', pl[2], pr[2]), power)
 
+    scope = root.get_scope()
+
     # Replace the left node with the new expression
     scope[scope.index(nl)] = replacement
 

+ 14 - 10
tests/test_node.py

@@ -51,23 +51,27 @@ class TestNode(unittest.TestCase):
         self.assertTrue(L(1.5).is_numeric())
         self.assertFalse(L('a').is_numeric())
 
-    def test_get_polynome_identifier(self):
-        self.assertEqual(L('a').get_polynome(), (L('a'), L(1), L(1), False))
+    def test_extract_polynome_properties_identifier(self):
+        self.assertEqual(L('a').extract_polynome_properties(),
+                         (L(1), L('a'), L(1)))
 
-    def test_get_polynome_None(self):
-        self.assertIsNone(N('+').get_polynome())
+    def test_extract_polynome_properties_None(self):
+        self.assertIsNone(N('+').extract_polynome_properties())
 
-    def test_get_polynome_power(self):
+    def test_extract_polynome_properties_power(self):
         power = N('^', L('a'), L(2))
-        self.assertEqual(power.get_polynome(), (L('a'), L(2), L(1), True))
+        self.assertEqual(power.extract_polynome_properties(),
+                         (L(1), L('a'), L(2)))
 
-    def test_get_polynome_coefficient_exponent_int(self):
+    def test_extract_polynome_properties_coefficient_exponent_int(self):
         times = N('*', L(3), N('^', L('a'), L(2)))
-        self.assertEqual(times.get_polynome(), (L('a'), L(2), L(3), True))
+        self.assertEqual(times.extract_polynome_properties(),
+                         (L(3), L('a'), L(2)))
 
-    def test_get_polynome_coefficient_exponent_id(self):
+    def test_extract_polynome_properties_coefficient_exponent_id(self):
         times = N('*', L(3), N('^', L('a'), L('b')))
-        self.assertEqual(times.get_polynome(), (L('a'), L('b'), L(3), True))
+        self.assertEqual(times.extract_polynome_properties(),
+                         (L(3), L('a'), L('b')))
 
     def test_get_scope_binary(self):
         plus = N('+', *self.l[:2])

+ 37 - 41
tests/test_rules_poly.py

@@ -1,7 +1,7 @@
 import unittest
 
 from src.node import ExpressionNode as N, ExpressionLeaf as L
-from src.rules.poly import match_combine_factors, combine_polynomes
+from src.rules.poly import match_combine_polynomes, combine_polynomes
 from src.possibilities import Possibility as P
 from src.parser import Parser
 from tests.parser import ParserWrapper
@@ -13,10 +13,10 @@ def tree(exp, **kwargs):
 
 class TestRulesPoly(unittest.TestCase):
 
-    #def test_match_combine_factors_numeric_combinations(self):
+    #def test_match_combine_polynomes_numeric_combinations(self):
     #    l0, l1, l2 = L(1), L(2), L(2)
     #    plus = N('+', N('+', l0, l1), l2)
-    #    p = match_combine_factors(plus)
+    #    p = match_combine_polynomes(plus)
     #    self.assertEqualPos(p, [P(plus, combine_polynomes, (l0, l1)),
     #                            P(plus, combine_polynomes, (l0, l2)),
     #                            P(plus, combine_polynomes, (l1, l2))])
@@ -32,63 +32,59 @@ class TestRulesPoly(unittest.TestCase):
 
             self.assertEqual(p, e)
 
-    def test_basic(self):
+    def test_numbers(self):
         l1, l2 = root = tree('1+2')
-        self.assertEqualPos(match_combine_factors(root),
-                [P(root, combine_polynomes, ((l1, (l1, l1, l1, False)),
-                                             (l2, (l2, l1, l1, False))))])
+        self.assertEqualPos(match_combine_polynomes(root),
+                [P(root, combine_polynomes, ((l1, (l1, l1, l1)),
+                                             (l2, (l1, l2, l1))))])
 
+    def test_identifiers_basic(self):
+        l1, l2 = tree('1,2')
         a1, a2 = root = tree('a+a')
-        self.assertEqualPos(match_combine_factors(root),
-                [P(root, combine_polynomes, ((a1, (a1, l1, l1, False)),
-                                             (a2, (a2, l1, l1, False))))])
+        self.assertEqualPos(match_combine_polynomes(root),
+                [P(root, combine_polynomes, ((a1, (l1, a1, l1)),
+                                             (a2, (l1, a2, l1))))])
 
+    def test_identifiers_normal(self):
+        l1, l2 = tree('1,2')
         a1, a2 = root = tree('a+2a')
-        self.assertEqualPos(match_combine_factors(root),
-                [P(root, combine_polynomes, ((a1, (a1, l1, l1, False)),
-                                             (a2, (a2[1], l1, l2, False))))])
+        self.assertEqualPos(match_combine_polynomes(root),
+                [P(root, combine_polynomes, ((a1, (l1, a1, l1)),
+                                             (a2, (l2, a2[1], l1))))])
 
+    def test_identifiers_reverse(self):
+        l1, l2, la = tree('1,2,a')
         a1, a2 = root = tree('a+a*2')
-        self.assertEqualPos(match_combine_factors(root),
-                [P(root, combine_polynomes, ((a1, (a1, l1, l1, False)),
-                                             (a2, (a2[1], l1, l2, False))))])
+        self.assertEqualPos(match_combine_polynomes(root),
+                [P(root, combine_polynomes, ((a1, (l1, a1, l1)),
+                                             (a2, (l2, la, l1))))])
 
+    def test_identifiers_exponent(self):
+        l1, l2 = tree('1,2')
         a1, a2 = root = tree('a2+a2')
-        self.assertEqualPos(match_combine_factors(root),
-                [P(root, combine_polynomes, ((a1, (a1[0], l2, l1, True)),
-                                             (a2, (a2[0], l2, l1, True))))])
+        self.assertEqualPos(match_combine_polynomes(root),
+                [P(root, combine_polynomes, ((a1, (l1, a1[0], l2)),
+                                             (a2, (l1, a2[0], l2))))])
 
 
     def test_basic_subexpressions(self):
-        return # TODO: test this!!
-        a_b = tree('a+b')
-        c, d = tree('c+d')
-        l1 = tree('1')
-        l5, l7 = tree('5+7')
-
+        a_b, c, d, l1, l5, l7 = tree('a+b,c,d,1,5,7')
         left, right = root = tree('(a+b)^d + (a+b)^d')
 
         self.assertEqual(left, right)
-        self.assertEqualPos(match_combine_factors(root),
-                [P(root, combine_polynomes, ((left, (a_b, d, l1, True)),
-                                             (right, (a_b, d, l1, True))))])
+        self.assertEqualPos(match_combine_polynomes(root),
+                [P(root, combine_polynomes, ((left, (l1, a_b, d)),
+                                             (right, (l1, a_b, d))))])
 
         left, right = root = tree('5(a+b)^d + 7(a+b)^d')
 
-        #<Possibility root="5 * (a + b) ^ d + 7 * (a + b) ^ d"
-        #    handler=combine_polynomes args=((<src.node.ExpressionNode object at
-        #    0x9fb2e0c>, (<src.node.ExpressionNode object at 0x9fb2c2c>,
-        #    'd', 5, True)), (<src.node.ExpressionNode object at
-        #    0x9fb438c>, (<src.node.ExpressionNode object at
-        #    0x9fb2f0c>, 'd', 7, True)))>
-
-        self.assertEqualPos(match_combine_factors(root),
-                [P(root, combine_polynomes, ((left, (a_b, d, l5, True)),
-                                             (right, (a_b, d, l7, True))))])
+        self.assertEqualPos(match_combine_polynomes(root),
+                [P(root, combine_polynomes, ((left, (l5, a_b, d)),
+                                             (right, (l7, a_b, d))))])
 
         left, right = root = tree('c(a+b)^d + c(a+b)^d')
 
         self.assertEqual(left, right)
-        self.assertEqualPos(match_combine_factors(root),
-                [P(root, combine_polynomes, ((left, (left[0], c, d, True)),
-                                             (right, (right[0], c, d, True))))])
+        self.assertEqualPos(match_combine_polynomes(root),
+                [P(root, combine_polynomes, ((left, (c, a_b, d)),
+                                             (right, (c, a_b, d))))])