Przeglądaj źródła

Merge branch 'negated'

Taddeus Kroes 14 lat temu
rodzic
commit
41047c7e93

+ 1 - 1
external/graph_drawing

@@ -1 +1 @@
-Subproject commit 84ad376b81ac72e163bacd7b538df16cac9be153
+Subproject commit 821bdb8f8408fb36ee1d92ada162589794a8c5b3

+ 106 - 37
src/node.py

@@ -29,9 +29,21 @@ OP_MOD = 7
 
 # N-ary (functions)
 OP_INT = 8
-OP_EXPAND = 9
-OP_COMMA = 10
-OP_SQRT = 11
+OP_COMMA = 9
+OP_SQRT = 10
+
+# Goniometry
+OP_SIN = 11
+OP_COS = 12
+OP_TAN = 13
+
+OP_SOLVE = 14
+OP_EQ = 15
+
+OP_POSSIBILITIES = 16
+OP_HINT = 17
+OP_REWRITE_ALL = 18
+OP_REWRITE = 19
 
 
 TYPE_MAP = {
@@ -41,19 +53,45 @@ TYPE_MAP = {
         }
 
 OP_MAP = {
+        ',': OP_COMMA,
         '+': OP_ADD,
-        # Either substraction or negation. Skip the operator sign in 'x' (= 2).
         '-': OP_SUB,
         '*': OP_MUL,
         '/': OP_DIV,
         '^': OP_POW,
-        'mod': OP_MOD,
-        'int': OP_INT,
-        'expand': OP_EXPAND,
+        'sin': OP_SIN,
+        'cos': OP_COS,
+        'tan': OP_TAN,
         'sqrt': OP_SQRT,
-        ',': OP_COMMA,
+        'int': OP_INT,
+        'solve': OP_SOLVE,
+        '=': OP_EQ,
+        '??': OP_POSSIBILITIES,
+        '?': OP_HINT,
+        '@@': OP_REWRITE_ALL,
+        '@': OP_REWRITE,
         }
 
+TOKEN_MAP = {
+        OP_COMMA: 'COMMA',
+        OP_ADD: 'PLUS',
+        OP_SUB: 'MINUS',
+        OP_MUL: 'TIMES',
+        OP_DIV: 'DIVIDE',
+        OP_POW: 'POW',
+        OP_SQRT: 'SQRT',
+        OP_SIN: 'SIN',
+        OP_COS: 'COS',
+        OP_TAN: 'TAN',
+        OP_INT: 'INT',
+        OP_SOLVE: 'SOLVE',
+        OP_EQ: 'EQ',
+        OP_POSSIBILITIES: 'POSSIBILITIES',
+        OP_HINT: 'HINT',
+        OP_REWRITE_ALL: 'REWRITE_ALL',
+        OP_REWRITE: 'REWRITE',
+}
+
 
 def to_expression(obj):
     return obj if isinstance(obj, ExpressionBase) else ExpressionLeaf(obj)
@@ -112,8 +150,11 @@ class ExpressionBase(object):
     def is_op(self, op):
         return not self.is_leaf and self.op == op
 
-    def is_power(self):
-        return not self.is_leaf and self.op == OP_POW
+    def is_power(self, exponent=None):
+        if self.is_leaf or self.op != OP_POW:
+            return False
+
+        return exponent == None or self[1] == exponent
 
     def is_nary(self):
         return not self.is_leaf and self.op in [OP_ADD, OP_SUB, OP_MUL]
@@ -145,8 +186,13 @@ class ExpressionBase(object):
     def __pow__(self, other):
         return ExpressionNode('^', self, to_expression(other))
 
+    def __pos__(self):
+        return self.reduce_negation()
+
     def reduce_negation(self, n=1):
         """Remove n negation flags from the node."""
+        assert self.negated
+
         return self.negate(-n)
 
     def negate(self, n=1):
@@ -160,9 +206,6 @@ class ExpressionNode(Node, ExpressionBase):
         self.type = TYPE_OPERATOR
         self.op = OP_MAP[args[0]]
 
-        if hasattr(self.op, '__call__'):
-            self.op = self.op(args)
-
     def __str__(self):  # pragma: nocover
         return generate_line(self)
 
@@ -170,10 +213,8 @@ class ExpressionNode(Node, ExpressionBase):
         """
         Check strict equivalence.
         """
-        if isinstance(other, ExpressionNode):
-            return self.op == other.op and self.nodes == other.nodes
-
-        return False
+        return isinstance(other, ExpressionNode) and self.op == other.op \
+               and self.negated == other.negated and self.nodes == other.nodes
 
     def substitute(self, old_child, new_child):
         self.nodes[self.nodes.index(old_child)] = new_child
@@ -245,7 +286,7 @@ class ExpressionNode(Node, ExpressionBase):
             return (self[0], self[1], ExpressionLeaf(1))
         return (self[1], self[0], ExpressionLeaf(1))
 
-    def equals(self, other):
+    def equals(self, other, ignore_negation=False):
         """
         Perform a non-strict equivalence check between two nodes:
         - If the other node is a leaf, it cannot be equal to this node.
@@ -256,18 +297,14 @@ class ExpressionNode(Node, ExpressionBase):
         - If both nodes are divisions, the nominator and denominator have to be
           non-strictly equal.
         """
-        if not other.is_op(self.op):
-            # FIXME: this is if-clause is a problem. To fix this problem
-            # permanently, normalize ("x * -1" -> "-1x") before comparing to
-            # the other node.
-
+        if not isinstance(other, ExpressionNode) or other.op != self.op:
             return False
 
         if self.op in (OP_ADD, OP_MUL):
             s0 = Scope(self)
             s1 = set(Scope(other))
 
-            # Scopes sould be of equal size
+            # Scopes should be of equal size
             if len(s0) != len(s1):
                 return False
 
@@ -291,7 +328,10 @@ class ExpressionNode(Node, ExpressionBase):
                 if not child.equals(other[i]):
                     return False
 
-        return True
+        if ignore_negation:
+            return True
+
+        return self.negated == other.negated
 
 
 class ExpressionLeaf(Leaf, ExpressionBase):
@@ -306,16 +346,33 @@ class ExpressionLeaf(Leaf, ExpressionBase):
         other_type = type(other)
 
         if other_type in TYPE_MAP:
-            return TYPE_MAP[other_type] == self.type and self.value == other
+            return TYPE_MAP[other_type] == self.type \
+                   and self.actual_value() == other
 
-        return other.type == self.type and self.value == other.value
+        return self.negated == other.negated and self.type == other.type \
+               and self.value == other.value
 
-    def equals(self, other):
+    def __repr__(self):
+        return '-' * self.negated + str(self.value)
+
+    def equals(self, other, ignore_negation=False):
         """
         Check non-strict equivalence.
-        Between leaves, this is the same as strict equivalence.
+        Between leaves, this is the same as strict equivalence, except when
+        negations must be ignored.
         """
-        return self == other
+        if ignore_negation:
+            other_type = type(other)
+
+            if other_type in (int, float):
+                return TYPE_MAP[other_type] == self.type \
+                    and self.value == abs(other)
+            elif other_type == str:
+                return self.type == TYPE_IDENTIFIER and self.value == other
+
+            return self.type == other.type and self.value == other.value
+        else:
+            return self == other
 
     def extract_polynome_properties(self):
         """
@@ -350,7 +407,14 @@ class Scope(object):
     def __iter__(self):
         return iter(self.nodes)
 
-    def remove(self, node, replacement=None):
+    def __eq__(self, other):
+        return isinstance(other, Scope) and self.node == other.node \
+               and self.nodes == other.nodes
+
+    def __repr__(self):
+        return '<Scope of "%s">' % repr(self.node)
+
+    def remove(self, node, **kwargs):
         if node.is_leaf:
             node_cmp = hash(node)
         else:
@@ -363,8 +427,8 @@ class Scope(object):
                 n_cmp = n
 
             if n_cmp == node_cmp:
-                if replacement != None:
-                    self[i] = replacement
+                if 'replacement' in kwargs:
+                    self[i] = kwargs['replacement']
                 else:
                     del self.nodes[i]
 
@@ -373,8 +437,11 @@ class Scope(object):
         raise ValueError('Node "%s" is not in the scope of "%s".'
                          % (node, self.node))
 
+    def replace(self, node, replacement):
+        self.remove(node, replacement=replacement)
+
     def as_nary_node(self):
-        return nary_node(self.node.value, self.nodes)
+        return nary_node(self.node.value, self.nodes).negate(self.node.negated)
 
 
 def nary_node(operator, scope):
@@ -405,7 +472,9 @@ def get_scope(node):
 
 def negate(node, n=1):
     """Negate the given node n times."""
-    node = node.clone()
-    node.negated = n
+    assert n >= 0
+
+    new_node = node.clone()
+    new_node.negated = n
 
-    return node
+    return new_node

+ 46 - 45
src/parser.py

@@ -3,8 +3,6 @@ This parser will parse the given input and build an expression tree. Grammar
 file for the supported mathematical expressions.
 """
 
-from node import ExpressionNode as Node, ExpressionLeaf as Leaf
-
 import os.path
 PYBISON_BUILD = os.path.realpath('build/external/pybison')
 EXTERNAL_MODS = os.path.realpath('external')
@@ -16,7 +14,8 @@ sys.path.insert(1, EXTERNAL_MODS)
 from pybison import BisonParser, BisonSyntaxError
 from graph_drawing.graph import generate_graph
 
-from node import TYPE_OPERATOR, OP_COMMA, OP_NEG
+from node import ExpressionNode as Node, ExpressionLeaf as Leaf, OP_MAP, \
+        TOKEN_MAP, TYPE_OPERATOR, OP_COMMA, OP_NEG, OP_MUL, Scope
 from rules import RULES
 from possibilities import filter_duplicates, pick_suggestion, apply_suggestion
 
@@ -52,10 +51,8 @@ class Parser(BisonParser):
     # ----------------------------------------------------------------
     # TODO: add a runtime check to verify that this token list match the list
     # of tokens of the lex script.
-    tokens = ['NUMBER', 'IDENTIFIER', 'POSSIBILITIES',
-              'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'POW',
-              'LPAREN', 'RPAREN', 'COMMA', 'HINT', 'REWRITE',
-              'NEWLINE', 'QUIT', 'RAISE', 'GRAPH', 'SQRT']
+    tokens =  ['NUMBER', 'IDENTIFIER', 'NEWLINE', 'QUIT', 'RAISE', 'GRAPH', \
+               'LPAREN', 'RPAREN'] + TOKEN_MAP.values()
 
     # ------------------------------
     # precedences
@@ -74,13 +71,20 @@ class Parser(BisonParser):
         BisonParser.__init__(self, **kwargs)
         self.interactive = kwargs.get('interactive', 0)
         self.timeout = kwargs.get('timeout', 0)
-        self.possibilities = self.last_possibilities = []
 
+        self.reset()
+
+    def reset(self):
         self.read_buffer = ''
         self.read_queue = Queue.Queue()
 
-        self.subtree_map = {}
+        #self.subtree_map = {}
         self.root_node = None
+        self.possibilities = self.last_possibilities = []
+
+    def run(self, *args, **kwargs):
+        self.reset()
+        return super(Parser, self).run(*args, **kwargs)
 
     # Override default read method with a version that prompts for input.
     def read(self, nbytes):
@@ -186,32 +190,16 @@ class Parser(BisonParser):
         if not retval.negated and retval.type != TYPE_OPERATOR:
             return retval
 
-        if self.subtree_map and retval.type == TYPE_OPERATOR:
-            # Update the subtree map to let the subtree point to its parent
-            # node.
-            parent_nodes = self.subtree_map.keys()
-
-            for child in retval:
-                if child in parent_nodes:
-                    self.subtree_map[child] = retval
-
         if retval.type == TYPE_OPERATOR and retval.op in RULES:
             handlers = RULES[retval.op]
         else:
             handlers = []
 
         if retval.negated:
-            handlers += RULES[OP_NEG]
+            handlers = RULES[OP_NEG]
 
         for handler in handlers:
             possibilities = handler(retval)
-
-            # Record the subtree root node in order to avoid tree traversal.
-            # At this moment, the node is the root node since the expression is
-            # parser using the left-innermost parsing strategy.
-            for p in possibilities:
-                self.subtree_map[p.root] = None
-
             self.possibilities.extend(possibilities)
 
         return retval
@@ -231,8 +219,7 @@ class Parser(BisonParser):
         if not suggestion:
             return self.root_node
 
-        expression = apply_suggestion(self.root_node, self.subtree_map,
-                                    suggestion)
+        expression = apply_suggestion(self.root_node, suggestion)
 
         if self.verbose:
             print 'After application, expression=', expression
@@ -350,9 +337,14 @@ class Parser(BisonParser):
         """
 
         if option == 0:  # rule: NEG exp
-            node = values[1]
-            node.negated += 1
-            return node
+            # Add negation to the left-most child
+            if values[1].is_leaf or values[1].op != OP_MUL:
+                values[1].negated += 1
+            else:
+                child = Scope(values[1])[0]
+                child.negated += 1
+
+            return values[1]
 
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
                                % (option, target))  # pragma: nocover
@@ -371,8 +363,18 @@ class Parser(BisonParser):
 
         if option == 4:  # rule: exp MINUS exp
             node = values[2]
-            node.negated += 1
-            return Node('+', values[0], node)
+
+            # Add negation to the left-most child
+            if node.is_leaf or node.op != OP_MUL:
+                node.negated += 1
+            else:
+                node = Scope(node)[0]
+                node.negated += 1
+
+            # Explicit call the hook handler on the created unary negation.
+            node = self.hook_handler('binary', 4, names, values, node)
+
+            return Node('+', values[0], values[2])
 
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
                                % (option, target))  # pragma: nocover
@@ -388,6 +390,15 @@ class Parser(BisonParser):
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
                                % (option, target))  # pragma: nocover
 
+    # -----------------------------------------
+    # operator tokens
+    # -----------------------------------------
+    operators = ''
+
+    for op_str, op in OP_MAP.iteritems():
+        operators += '"%s"%s{ returntoken(%s); }\n' \
+                     % (op_str, ' ' * (8 - len(op_str)), TOKEN_MAP[op])
+
     # -----------------------------------------
     # raw lex script, verbatim here
     # -----------------------------------------
@@ -415,8 +426,6 @@ class Parser(BisonParser):
             yylloc.first_column = yycolumn; \
             yylloc.last_column = yycolumn + yyleng; \
             yycolumn += yyleng;
-
-    /*[a-zA-Z][0-9]+ { returntoken(CONCAT_POW); }*/
     %}
 
     %option yylineno
@@ -427,19 +436,11 @@ class Parser(BisonParser):
     [a-zA-Z]  { returntoken(IDENTIFIER); }
     "("       { returntoken(LPAREN); }
     ")"       { returntoken(RPAREN); }
-    "+"       { returntoken(PLUS); }
-    "-"       { returntoken(MINUS); }
-    "*"       { returntoken(TIMES); }
-    "^"       { returntoken(POW); }
-    "/"       { returntoken(DIVIDE); }
     ","       { returntoken(COMMA); }
-    "??"      { returntoken(POSSIBILITIES); }
-    "?"       { returntoken(HINT); }
-    "@"       { returntoken(REWRITE); }
-    "quit"    { yyterminate(); returntoken(QUIT); }
+    """ + operators + r"""
     "raise"   { returntoken(RAISE); }
     "graph"   { returntoken(GRAPH); }
-    "sqrt"    { returntoken(SQRT); }
+    "quit"    { yyterminate(); returntoken(QUIT); }
 
     [ \t\v\f] { }
     [\n]      { yycolumn = 0; returntoken(NEWLINE); }

+ 26 - 5
src/possibilities.py

@@ -1,3 +1,6 @@
+from node import TYPE_OPERATOR
+
+
 # Each rule will append its hint message to the following dictionary. The
 # function pointer to the apply function of the rule is used as key. The
 # corresponding value is a string, which will be used to produce the hint
@@ -60,7 +63,27 @@ def pick_suggestion(possibilities):
     return possibilities[suggestion]
 
 
-def apply_suggestion(root, subtree_map, suggestion):
+def find_parent_node(root, child):
+    nodes = [root]
+
+    while nodes:
+        node = nodes.pop()
+
+        while node:
+
+            if node.type != TYPE_OPERATOR:
+                break
+
+            if child in node:
+                return node
+
+            if len(node) > 1:
+                nodes.append(node[1])
+
+            node = node[0]
+
+
+def apply_suggestion(root, suggestion):
     # TODO: clone the root node before modifying. After deep copying the root
     # node, the subtree_map cannot be used since the hash() of each node in the
     # deep copied root node has changed.
@@ -68,10 +91,7 @@ def apply_suggestion(root, subtree_map, suggestion):
 
     subtree = suggestion.handler(suggestion.root, suggestion.args)
 
-    if suggestion.root in subtree_map:
-        parent_node = subtree_map[suggestion.root]
-    else:
-        parent_node = None
+    parent_node = find_parent_node(root, suggestion.root)
 
     # There is either a parent node or the subtree is the root node.
     # FIXME: FAIL: test_diagnostic_test_application in tests/test_b1_ch08.py
@@ -85,4 +105,5 @@ def apply_suggestion(root, subtree_map, suggestion):
     if parent_node:
         parent_node.substitute(suggestion.root, subtree)
         return root
+
     return subtree

+ 14 - 13
src/rules/__init__.py

@@ -1,26 +1,27 @@
 from ..node import OP_ADD, OP_MUL, OP_DIV, OP_POW, OP_NEG
-from .poly import match_combine_polynomes
 from .groups import match_combine_groups
 from .factors import match_expand
 from .powers import match_add_exponents, match_subtract_exponents, \
         match_multiply_exponents, match_duplicate_exponent, \
         match_remove_negative_exponent, match_exponent_to_root, \
-        match_extend_exponent
-from .numerics import match_divide_numerics, match_multiply_numerics, \
-        match_multiply_zero
+        match_extend_exponent, match_constant_exponent
+from .numerics import match_add_numerics, match_divide_numerics, \
+        match_multiply_numerics, match_multiply_zero, match_multiply_one
 from .fractions import match_constant_division, match_add_constant_fractions, \
         match_expand_and_add_fractions
-from .negation import match_negate_group, match_negated_division
+from .negation import match_negated_factor, match_negate_polynome, \
+        match_negated_division
 
 RULES = {
-        OP_ADD: [match_add_constant_fractions, match_combine_polynomes, \
+        OP_ADD: [match_add_numerics, match_add_constant_fractions,
                  match_combine_groups],
-        OP_MUL: [match_multiply_numerics, match_expand, match_add_exponents, \
-                 match_expand_and_add_fractions, match_multiply_zero],
-        OP_DIV: [match_subtract_exponents, match_divide_numerics, \
+        OP_MUL: [match_multiply_numerics, match_expand, match_add_exponents,
+                 match_expand_and_add_fractions, match_multiply_zero,
+                 match_negated_factor, match_multiply_one],
+        OP_DIV: [match_subtract_exponents, match_divide_numerics,
                  match_constant_division, match_negated_division],
-        OP_POW: [match_multiply_exponents, match_duplicate_exponent, \
-                 match_remove_negative_exponent, match_exponent_to_root, \
-                 match_extend_exponent],
-        OP_NEG: [match_negate_group],
+        OP_POW: [match_multiply_exponents, match_duplicate_exponent,
+                 match_remove_negative_exponent, match_exponent_to_root,
+                 match_extend_exponent, match_constant_exponent],
+        OP_NEG: [match_negate_polynome],
         }

+ 6 - 2
src/rules/factors.py

@@ -21,6 +21,10 @@ def match_expand(node):
         if n.is_leaf:
             leaves.append(n)
         elif n.op == OP_ADD:
+            # If the addition only contains numerics, do not expand
+            if not filter(lambda n: not n.is_numeric(), Scope(n)):
+                continue
+
             additions.append(n)
 
     for args in product(leaves, additions):
@@ -45,7 +49,7 @@ def expand_single(root, args):
     scope = Scope(root)
 
     # Replace 'a' with the new expression
-    scope.remove(a, a * b + a * c)
+    scope.replace(a, a * b + a * c)
 
     # Remove the addition
     scope.remove(bc)
@@ -66,7 +70,7 @@ def expand_double(root, args):
     scope = Scope(root)
 
     # Replace 'a + b' with the new expression
-    scope.remove(ab, a * c + a * d + b * c + b * d)
+    scope.replace(ab, a * c + a * d + b * c + b * d)
 
     # Remove the right addition
     scope.remove(cd)

+ 10 - 5
src/rules/fractions.py

@@ -1,7 +1,7 @@
 from itertools import combinations
 
 from .utils import least_common_multiple
-from ..node import ExpressionLeaf as L, Scope, OP_DIV, OP_ADD, OP_MUL
+from ..node import ExpressionLeaf as L, Scope, negate, OP_DIV, OP_ADD, OP_MUL
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 
@@ -112,10 +112,15 @@ def equalize_denominators(root, args):
         mult = denom / d.value
 
         if mult != 1:
-            n = L(n.value * mult) if n.is_numeric() else L(mult) * n
+            if n.is_numeric():
+                n = L(n.value * mult)
+            else:
+                n = L(mult) * n
 
-            scope.remove(fraction, negate(n / L(d.value * mult),
-                                          fraction.negated))
+            #n = L(n.value * mult) if n.is_numeric() else L(mult) * n
+
+            scope.replace(fraction, negate(n / L(d.value * mult),
+                                           fraction.negated))
 
     return scope.as_nary_node()
 
@@ -137,7 +142,7 @@ def add_nominators(root, args):
     scope = Scope(root)
 
     # Replace the left node with the new expression
-    scope.remove(ab, (a + negate(cb[0], cb.negated)) / b)
+    scope.replace(ab, (a + cb[0].negate(cb.negated)) / b)
 
     # Remove the right node
     scope.remove(cb)

+ 32 - 0
src/rules/goniometry.py

@@ -0,0 +1,32 @@
+from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, \
+        OP_ADD, OP_POW, OP_MUL, OP_SIN, OP_COS, OP_TAN
+from ..possibilities import Possibility as P, MESSAGES
+from ..translate import _
+
+
+def match_add_quadrants(node):
+    """
+    sin(x) ^ 2 + cos(x) ^ 2  ->  1
+    """
+    assert node.is_op(OP_ADD)
+
+    p = []
+    sin_q, cos_q = node
+
+    if sin_q.is_power(2) and cos_q.is_power(2):
+        sin, cos = sin_q[0], cos_q[0]
+
+        if sin.is_op(OP_SIN) and cos.is_op(OP_COS):
+            p.append(P(node, add_quadrants, ()))
+
+    return p
+
+
+def add_quadrants(root, args):
+    """
+    sin(x) ^ 2 + cos(x) ^ 2  ->  1
+    """
+    return L(1)
+
+
+MESSAGES[add_quadrants] = _('Add the sinus and cosinus quadrants to 1.')

+ 24 - 19
src/rules/groups.py

@@ -1,7 +1,7 @@
 from itertools import combinations
 
 from ..node import ExpressionNode as Node, ExpressionLeaf as Leaf, Scope, \
-        OP_ADD, OP_MUL
+        OP_ADD, OP_MUL, nary_node, negate
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 
@@ -22,44 +22,49 @@ def match_combine_groups(node):
 
     p = []
     groups = []
+    scope = Scope(node)
 
-    for n in Scope(node):
-        groups.append((1, n, n))
+    for n in scope:
+        if not n.is_numeric():
+            groups.append((Leaf(1), n, n))
 
         # Each number multiplication yields a group, multiple occurences of
         # the same group can be replaced by a single one
         if n.is_op(OP_MUL):
-            scope = Scope(n)
-            l = len(scope)
+            n_scope = Scope(n)
+            l = len(n_scope)
 
-            for i, sub_node in enumerate(scope):
+            for i, sub_node in enumerate(n_scope):
                 if sub_node.is_numeric():
-                    others = [scope[j] for j in range(i) + range(i + 1, l)]
+                    others = [n_scope[j] for j in range(i) + range(i + 1, l)]
 
                     if len(others) == 1:
                         g = others[0]
                     else:
-                        g = Node('*', *others)
+                        g = nary_node('*', others)
 
                     groups.append((sub_node, g, n))
 
-    for g0, g1 in combinations(groups, 2):
-        if g0[1].equals(g1[1]):
-            p.append(P(node, combine_groups, g0 + g1))
+    for (c0, g0, n0), (c1, g1, n1) in combinations(groups, 2):
+        if g0.equals(g1):
+            p.append(P(node, combine_groups, (scope, c0, g0, n0, c1, g1, n1)))
+        elif g0.equals(g1, ignore_negation=True):
+            # Move negations to constants
+            c0 = c0.negate(g0.negated)
+            c1 = c1.negate(g1.negated)
+            g0 = negate(g0, 0)
+            g1 = negate(g1, 0)
+
+            p.append(P(node, combine_groups, (scope, c0, g0, n0, c1, g1, n1)))
 
     return p
 
 
 def combine_groups(root, args):
-    c0, g0, n0, c1, g1, n1 = args
-
-    scope = Scope(root)
-
-    if not isinstance(c0, Leaf) and not isinstance(c0, Node):
-        c0 = Leaf(c0)
+    scope, c0, g0, n0, c1, g1, n1 = args
 
     # Replace the left node with the new expression
-    scope.remove(n0, (c0 + c1) * g0)
+    scope.replace(n0, (c0 + c1) * g0)
 
     # Remove the right node
     scope.remove(n1)
@@ -68,4 +73,4 @@ def combine_groups(root, args):
 
 
 MESSAGES[combine_groups] = \
-        _('Group "{2}" is multiplied by {1} and {4}, combine them.')
+        _('Group "{3}" is multiplied by {2} and {5}, combine them.')

+ 77 - 45
src/rules/negation.py

@@ -1,76 +1,108 @@
-from ..node import get_scope, nary_node, OP_ADD, OP_MUL, OP_DIV
+from ..node import Scope, OP_ADD, OP_MUL, OP_DIV
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 
 
-def match_negate_group(node):
+def match_negated_factor(node):
     """
-    --a                 ->  a
-    -(a * ... * -b)     ->  ab
-    -(a + b + ... + z)  ->  -a + -b + ... + -z
+    This rule assures that negations in the scope of a multiplication are
+    brought to the most left node in the multiplication's scope.
+
+    Example:
+    a * -b  ->  -(ab)
     """
-    assert node.negated
+    assert node.is_op(OP_MUL)
 
-    if node.negated == 2:
-        # --a
-        return [P(node, double_negation, (node,))]
+    p = []
+    scope = Scope(node)
 
-    if not node.is_leaf:
-        scope = get_scope(node)
+    # FIXME: The negation that is brought outside is assigned to the first
+    # element in the scope during the next parsing step:
+    # -ab -> -(ab), but -(ab) is printed as -ab
+    for factor in scope[1:]:
+        if factor.negated:
+            p.append(P(node, negated_factor, (scope, factor)))
 
-        if node.is_op(OP_MUL) and any(map(lambda n: n.negated, scope)):
-            # -(-a)b
-            return [P(node, negate_group, (node, scope))]
+    return p
 
-        if node.is_op(OP_ADD):
-            # -(ab + c)   ->  -ab - c
-            # -(-ab + c)  ->  ab - c
-            return [P(node, negate_polynome, (node, scope))]
 
-    return []
+def negated_factor(root, args):
+    """
+    a * -b  ->  -ab
+    """
+    scope, factor = args
+    scope[0] = -scope[0]
+    scope.replace(factor, +factor)
+
+    return scope.as_nary_node()
 
 
-def negate_group(root, args):
+MESSAGES[negated_factor] = \
+        _('Bring negation of {2} to the outside of the multiplication.')
+
+
+def match_negate_polynome(node):
     """
-    -(a * -3c)       ->  a * 3c
-    -(a * ... * -b)  ->  ab
+    --a       ->  a
+    -(a + b)  ->  -a - b
     """
-    node, scope = args
+    #print 'match_negate_polynome:', node, node.negated
+    assert node.negated, str(node.negated) + '; ' + str(node)
 
-    for i, n in enumerate(scope):
-        if n.negated:
-            scope[i] = n.reduce_negation()
+    p = []
+
+    if node.negated == 2:
+        # --a
+        p.append(P(node, double_negation, ()))
 
-    return nary_node('*', scope).reduce_negation()
+    if node.is_op(OP_ADD):
+        # -(a + b)  ->  -a - b
+        p.append(P(node, negate_polynome, ()))
 
+    return p
 
-MESSAGES[negate_group] = _('Apply negation to the polynome {1[0]}.')
+
+def double_negation(root, args):
+    """
+    --a  ->  a
+    """
+    return root.reduce_negation(2)
+
+
+MESSAGES[double_negation] = _('Remove double negation in {0}.')
 
 
 def negate_polynome(root, args):
     """
-    -(-ab + ... + c)  ->  --ab + ... + -c
+    -(a + b)  ->  -a - b
     """
-    node, scope = args
+    scope = Scope(root)
 
     # Negate each group
     for i, n in enumerate(scope):
         scope[i] = -n
 
-    return nary_node('+', scope)
+    return +scope.as_nary_node()
 
 
-MESSAGES[negate_polynome] = _('Apply negation to the subexpression {1[0]}.')
-
-
-def double_negation(root, args):
-    """
-    --a  ->  a
-    """
-    return negate(args[0], args[0].negated - 2)
+MESSAGES[negate_polynome] = _('Apply negation to the polynome {0}.')
 
 
-MESSAGES[double_negation] = _('Remove double negation in {1}.')
+#def negate_group(root, args):
+#    """
+#    -(a * -3c)       ->  a * 3c
+#    -(a * ... * -b)  ->  ab
+#    """
+#    node, scope = args
+#
+#    for i, n in enumerate(scope):
+#        if n.negated:
+#            scope[i] = n.reduce_negation()
+#
+#    return nary_node('*', scope).reduce_negation()
+#
+#
+#MESSAGES[negate_polynome] = _('Apply negation to the subexpression {1[0]}.')
 
 
 def match_negated_division(node):
@@ -82,11 +114,11 @@ def match_negated_division(node):
     a, b = node
 
     if a.negated and b.negated:
-        return [P(node, double_negated_division, (node,))]
+        return [P(node, double_negated_division, ())]
     elif a.negated:
-        return [P(node, single_negated_division, (a[0], b))]
+        return [P(node, single_negated_division, (+a, b))]
     elif b.negated:
-        return [P(node, single_negated_division, (a, b[0]))]
+        return [P(node, single_negated_division, (a, +b))]
 
     return []
 
@@ -113,11 +145,11 @@ def double_negated_division(root, args):
     """
     a, b = root
 
-    return a[0] / b[0]
+    return +a / +b
 
 
 MESSAGES[double_negated_division] = \
-        _('Eliminate top and bottom negation in {1}.')
+        _('Eliminate top and bottom negation in {0}.')
 
 
 # TODO: negated multiplication: -a * -b = ab

+ 77 - 26
src/rules/numerics.py

@@ -1,11 +1,12 @@
 from itertools import combinations
 
-from ..node import ExpressionLeaf as Leaf, Scope, negate, OP_DIV, OP_MUL
+from ..node import ExpressionLeaf as Leaf, Scope, negate, OP_ADD, OP_DIV, \
+        OP_MUL
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 
 
-def add_numerics(root, args):
+def match_add_numerics(node):
     """
     Combine two constants to a single constant in an n-ary addition.
 
@@ -15,19 +16,43 @@ def add_numerics(root, args):
     -2 + 3   ->  1
     -2 + -3  ->  -5
     """
-    n0, n1, c0, c1 = args
-    scope = Scope(root)
+    assert node.is_op(OP_ADD)
+
+    p = []
+    scope = Scope(node)
+    numerics = filter(lambda n: n.is_numeric(), scope)
+
+    for c0, c1 in combinations(numerics, 2):
+        p.append(P(node, add_numerics, (scope, c0, c1)))
+
+    return p
+
+
+def add_numerics(root, args):
+    """
+    2 + 3    ->  5
+    2 + -3   ->  -1
+    -2 + 3   ->  1
+    -2 + -3  ->  -5
+    """
+    scope, c0, c1 = args
+    value = c0.actual_value() + c1.actual_value()
+
+    if value < 0:
+        leaf = Leaf(-value).negate()
+    else:
+        leaf = Leaf(value)
 
     # Replace the left node with the new expression
-    scope.remove(n0, Leaf(c0.actual_value() + c1.actual_value()))
+    scope.replace(c0, Leaf(abs(value)).negate(int(value < 0)))
 
     # Remove the right node
-    scope.remove(n1)
+    scope.remove(c1)
 
     return scope.as_nary_node()
 
 
-MESSAGES[add_numerics] = _('Combine the constants {1} and {2}.')
+MESSAGES[add_numerics] = _('Add the constants {2} and {3}.')
 
 
 #def match_subtract_numerics(node):
@@ -130,6 +155,42 @@ def multiply_zero(root, args):
 MESSAGES[multiply_zero] = _('Multiplication with zero yields zero.')
 
 
+def match_multiply_one(node):
+    """
+    a * 1    ->  a
+    1 * a    ->  a
+    -1 * a   ->  -a
+    1 * -a   ->  -a
+    -1 * -a  ->  a
+    """
+    assert node.is_op(OP_MUL)
+
+    left, right = node
+
+    if left.value == 1:
+        return [P(node, multiply_one, (right, left))]
+
+    if right.value == 1:
+        return [P(node, multiply_one, (left, right))]
+
+    return []
+
+
+def multiply_one(root, args):
+    """
+    a * 1  ->  a
+    1 * a  ->  a
+    -1 * a   ->  -a
+    1 * -a   ->  -a
+    -1 * -a  ->  a
+    """
+    a, one = args
+    return a.negate(one.negated + root.negated)
+
+
+MESSAGES[multiply_one] = _('Multiplication with one yields the multiplicant.')
+
+
 def match_multiply_numerics(node):
     """
     3 * 2      ->  6
@@ -140,14 +201,11 @@ def match_multiply_numerics(node):
     assert node.is_op(OP_MUL)
 
     p = []
-    numerics = []
-
-    for n in Scope(node):
-        if n.is_numeric():
-            numerics.append((n, n.actual_value()))
+    scope = Scope(node)
+    numerics = filter(lambda n: n.is_numeric(), scope)
 
-    for (n0, v0), (n1, v1) in combinations(numerics, 2):
-        p.append(P(node, multiply_numerics, (n0, n1, v0, v1)))
+    for c0, c1 in combinations(numerics, 2):
+        p.append(P(node, multiply_numerics, (scope, c0, c1)))
 
     return p
 
@@ -159,24 +217,17 @@ def multiply_numerics(root, args):
     Example:
     2 * 3  ->  6
     """
-    n0, n1, v0, v1 = args
-    scope = []
-    value = v0 * v1
-
-    if value > 0:
-        substitution = Leaf(value)
-    else:
-        substitution = -Leaf(-value)
+    scope, c0, c1 = args
 
-    scope = Scope(root)
 
     # Replace the left node with the new expression
-    scope.remove(n0, substitution)
+    substitution = Leaf(c0.value * c1.value).negate(c0.negated + c1.negated)
+    scope.replace(c0, substitution)
 
     # Remove the right node
-    scope.remove(n1)
+    scope.remove(c1)
 
     return scope.as_nary_node()
 
 
-MESSAGES[multiply_numerics] = _('Multiply constant {1} with {2}.')
+MESSAGES[multiply_numerics] = _('Multiply constant {2} with {3}.')

+ 0 - 92
src/rules/poly.py

@@ -1,92 +0,0 @@
-from itertools import combinations
-
-from ..node import Scope, OP_ADD
-from ..possibilities import Possibility as P, MESSAGES
-from .numerics import add_numerics
-
-
-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
-    """
-    assert node.is_op(OP_ADD)
-
-    p = []
-
-    # Collect all nodes that can be combined:
-    # a ^ e     = 1 * a ^ e
-    # c * a     = c * a ^ 1
-    # c * a ^ e
-    # a         = 1 * a ^ 1
-    #
-    # Identifier nodes of all polynomes, tuple format is:
-    #   (root, exponent, coefficient, literal_coefficient)
-    polys = []
-
-    if verbose:  # pragma: nocover
-        print 'match combine factors:', node
-
-    for n in Scope(node):
-        polynome = n.extract_polynome_properties()
-
-        if verbose:  # pragma: nocover
-            print 'n:', n, 'polynome:', polynome
-
-        if polynome:
-            polys.append((n, polynome))
-
-    # Each combination of powers of the same value and polynome can be added
-    if len(polys) >= 2:
-        for left, right in combinations(polys, 2):
-            n0, p0 = left
-            n1, p1 = right
-            c0, r0, e0 = p0
-            c1, r1, e1 = p1
-
-            # Both numeric root and same exponent -> combine coefficients and
-            # roots, or: same root and exponent -> combine coefficients.
-            # TODO: Addition with zero, e.g. a + 0 -> a
-            if c0 == 1 and c1 == 1 and e0 == 1 and e1 == 1 \
-                    and all(map(lambda n: n.is_numeric(), [r0, r1])):
-                # 2 + 3    ->  5
-                # 2 + -3   ->  -1
-                # -2 + 3   ->  1
-                # -2 + -3  ->  -5
-                p.append(P(node, add_numerics, (n0, n1, r0, r1)))
-            elif c0.is_numeric() and c1.is_numeric() and r0 == r1 and e0 == e1:
-                # 2a + 2a -> 4a
-                # a + 2a -> 3a
-                # 2a + a -> 3a
-                # a + a -> 2a
-                p.append(P(node, combine_polynomes, (n0, n1, c0, c1, r0, e0)))
-
-    return p
-
-
-def combine_polynomes(root, args):
-    """
-    Combine two multiplications of any polynome in an n-ary plus.
-
-    Synopsis:
-    c0 * a ^ b + c1 * a ^ b -> (c0 + c1) * a ^ b
-    """
-    n0, n1, c0, c1, r, e = args
-
-    # a ^ 1 -> a
-    if e == 1:
-        power = r
-    else:
-        power = r ** e
-
-    scope = Scope(root)
-
-    # Replace the left node with the new expression:
-    # (c0 + c1) * a ^ b
-    # a, b and c are from 'left', d is from 'right'.
-    scope.remove(n0, (c0 + c1) * power)
-
-    # Remove the right node
-    scope.remove(n1)
-
-    return scope.as_nary_node()

+ 45 - 8
src/rules/powers.py

@@ -17,8 +17,9 @@ def match_add_exponents(node):
 
     p = []
     powers = {}
+    scope = Scope(node)
 
-    for n in Scope(node):
+    for n in scope:
         if n.is_identifier():
             s = n
             exponent = L(1)
@@ -26,7 +27,7 @@ def match_add_exponents(node):
             # Order powers by their roots, e.g. a^p and a^q are put in the same
             # list because of the mutual 'a'
             s, exponent = n
-        else:
+        else:  # pragma: nocover
             continue
 
         s_str = str(s)
@@ -41,7 +42,7 @@ def match_add_exponents(node):
         # create a single power with that root
         if len(occurrences) > 1:
             for (n0, e1, a0), (n1, e2, a1) in combinations(occurrences, 2):
-                p.append(P(node, add_exponents, (n0, n1, a0, e1, e2)))
+                p.append(P(node, add_exponents, (scope, n0, n1, a0, e1, e2)))
 
     return p
 
@@ -50,11 +51,10 @@ def add_exponents(root, args):
     """
     a^p * a^q  ->  a^(p + q)
     """
-    n0, n1, a, p, q = args
-    scope = Scope(root)
+    scope, n0, n1, a, p, q = args
 
     # Replace the left node with the new expression
-    scope.remove(n0, a ** (p + q))
+    scope.replace(n0, a ** (p + q))
 
     # Remove the right node
     scope.remove(n1)
@@ -62,7 +62,7 @@ def add_exponents(root, args):
     return scope.as_nary_node()
 
 
-MESSAGES[add_exponents] = _('Add the exponents of {1} and {2}.')
+MESSAGES[add_exponents] = _('Add the exponents of {2} and {3}.')
 
 
 def match_subtract_exponents(node):
@@ -162,7 +162,7 @@ MESSAGES[duplicate_exponent] = _('Duplicate the exponent {2}.')
 
 def match_remove_negative_exponent(node):
     """
-    a^-p  ->  1 / a^p
+    a ^ -p  ->  1 / a ^ p
     """
     assert node.is_op(OP_POW)
 
@@ -237,3 +237,40 @@ def extend_exponent(root, args):
         return left * left ** L(right.value - 1)
 
     return left * left
+
+
+def match_constant_exponent(node):
+    """
+    (a + ... + z)^n -> (a + ... + z)(a + ... + z)^(n - 1)  # n > 1
+    """
+    assert node.is_op(OP_POW)
+
+    exponent = node[1]
+
+    if exponent == 0:
+        return [P(node, remove_power_of_zero, ())]
+
+    if exponent == 1:
+        return [P(node, remove_power_of_one, ())]
+
+    return []
+
+
+def remove_power_of_zero(root, args):
+    """
+    a ^ 0  ->  1
+    """
+    return L(1)
+
+
+MESSAGES[remove_power_of_zero] = _('Power of zero {0} rewrites to 1.')
+
+
+def remove_power_of_one(root, args):
+    """
+    a ^ 1  ->  a
+    """
+    return root[0]
+
+
+MESSAGES[remove_power_of_one] = _('Remove the power of one in {0}.')

+ 19 - 9
tests/rulestestcase.py

@@ -44,16 +44,26 @@ class RulesTestCase(unittest.TestCase):
         try:
             for i, exp in enumerate(rewrite_chain[:-1]):
                 self.assertMultiLineEqual(str(rewrite(exp)),
-                                          str(rewrite_chain[i+1]))
-        except AssertionError:  # pragma: nocover
-            print 'rewrite failed: "%s"  ->  "%s"' \
-                    % (str(exp), str(rewrite_chain[i+1]))
-            print 'rewrite chain index: %d' % i
-            print 'rewrite chain: ---'
+                                          str(rewrite_chain[i + 1]))
+        except AssertionError as e:  # pragma: nocover
+            msg = e.args[0]
 
-            for i, c in enumerate(rewrite_chain):
-                print '%2d  %s' % (i, str(c))
+            msg += '-' * 30 + '\n'
 
-            print '-' * 30
+            msg += 'rewrite failed: "%s"  ->  "%s"\n' \
+                         % (str(exp), str(rewrite_chain[i + 1]))
+
+            msg += 'rewrite chain: ---\n'
+
+            chain = []
+
+            for j, c in enumerate(rewrite_chain):
+                if i == j:
+                    chain.append('%2d  %s   <-- error' % (j, str(c)))
+                else:
+                    chain.append('%2d  %s' % (j, str(c)))
+
+            e.message = msg + '\n'.join(chain)
+            e.args = (e.message,) + e.args[1:]
 
             raise

+ 3 - 3
tests/test_b1_ch08.py

@@ -11,13 +11,13 @@ class TestB1Ch08(unittest.TestCase):
         run_expressions(Parser, [
             ('6*5^2', L(6) * L(5) ** 2),
             ('-5*(-3)^2', (-L(5)) * (-L(3)) ** 2),
-            ('7p-3p', L(7) * 'p' + -(L(3) * 'p')),
+            ('7p-3p', L(7) * 'p' + (-L(3) * 'p')),
             ('-5a*-6', (-L(5)) * 'a' * (-L(6))),
-            ('3a-8--5-2a', L(3) * 'a' + -L(8) + -(-L(5)) + -(L(2) * 'a')),
+            ('3a-8--5-2a', L(3) * 'a' + -L(8) + (--L(5)) + (-L(2) * 'a')),
             ])
 
     def test_diagnostic_test_application(self):
         apply_expressions(Parser, [
             ('7p+2p', 1, (L(7) + 2) * 'p'),
-            #('7p-3p', 1, (L(7) - 3) * 'p'),
+            ('7p-3p', 1, (L(7) + -L(3)) * 'p'),
             ])

+ 3 - 3
tests/test_b1_ch10.py

@@ -9,12 +9,12 @@ class TestB1Ch10(unittest.TestCase):
 
     def test_diagnostic_test(self):
         run_expressions(Parser, [
-            ('5(a-2b)', L(5) * (L('a') + -(L(2) * 'b'))),
+            ('5(a-2b)', L(5) * (L('a') + (-L(2) * 'b'))),
             ('-(3a+6b)', -(L(3) * L('a') + L(6) * 'b')),
             ('18-(a-12)', L(18) + -(L('a') + -L(12))),
             ('-p-q+5(p-q)-3q-2(p-q)',
-                -L('p') + -L('q') + L(5) * (L('p') + -L('q')) + -(L(3) * 'q') \
-                + - (L(2) * (L('p') + -L('q')))
+                -L('p') + -L('q') + L(5) * (L('p') + -L('q')) + (-L(3) * 'q') \
+                + (-L(2) * (L('p') + -L('q')))
             ),
             ('(2+3/7)^4',
                 N('^', N('+', L(2), N('/', L(3), L(7))), L(4))

+ 2 - 2
tests/test_calc.py

@@ -12,11 +12,11 @@ class TestCalc(unittest.TestCase):
                 == N('+', L(1), L(4))
 
     def test_basic_on_exp(self):
-        expressions = [('4',   L(4)),
+        expressions = [('4', L(4)),
                        ('3+4', L(3) + L(4)),
                        ('3-4', L(3) + -L(4)),
                        ('3/4', L(3) / L(4)),
-                       ('-4',  -L(4)),
+                       ('-4', -L(4)),
                        ('3^4', N('^', L(3), L(4))),
                        ('(2)', L(2))]
 

+ 1 - 6
tests/test_exception.py

@@ -7,9 +7,4 @@ from tests.parser import ParserWrapper
 
 class TestException(unittest.TestCase):
     def test_raise(self):
-        try:
-            ParserWrapper(Parser).run(['raise'])
-        except RuntimeError:
-            return
-
-        raise AssertionError('Expected raised RuntimeError!') # pragma: nocover
+        self.assertRaises(RuntimeError, ParserWrapper(Parser).run, ['raise'])

+ 58 - 24
tests/test_leiden_oefenopgave.py

@@ -30,6 +30,8 @@ class TestLeidenOefenopgave(TestCase):
                 '(xx + x * 1 + 1x + 1 * 1)(x + 1)',
                 '(x ^ (1 + 1) + x * 1 + 1x + 1 * 1)(x + 1)',
                 '(x ^ 2 + x * 1 + 1x + 1 * 1)(x + 1)',
+                '(x ^ 2 + x + 1x + 1 * 1)(x + 1)',
+                '(x ^ 2 + x + x + 1 * 1)(x + 1)',
                 '(x ^ 2 + (1 + 1)x + 1 * 1)(x + 1)',
                 '(x ^ 2 + 2x + 1 * 1)(x + 1)',
                 '(x ^ 2 + 2x + 1)(x + 1)',
@@ -40,9 +42,11 @@ class TestLeidenOefenopgave(TestCase):
                 'x ^ 3 + x ^ (1 + 1) * 2 + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
                 'x ^ 3 + x ^ 2 * 2 + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
                 'x ^ 3 + x ^ 2 * 2 + 1 * x ^ 2 + 1 * 2x + 1x + 1 * 1',
+                'x ^ 3 + x ^ 2 * 2 + x ^ 2 + 1 * 2x + 1x + 1 * 1',
                 'x ^ 3 + (2 + 1) * x ^ 2 + 1 * 2x + 1x + 1 * 1',
                 'x ^ 3 + 3 * x ^ 2 + 1 * 2x + 1x + 1 * 1',
                 'x ^ 3 + 3 * x ^ 2 + 2x + 1x + 1 * 1',
+                'x ^ 3 + 3 * x ^ 2 + 2x + x + 1 * 1',
                 'x ^ 3 + 3 * x ^ 2 + (2 + 1)x + 1 * 1',
                 'x ^ 3 + 3 * x ^ 2 + 3x + 1 * 1',
                 'x ^ 3 + 3 * x ^ 2 + 3x + 1',
@@ -56,6 +60,8 @@ class TestLeidenOefenopgave(TestCase):
                        'xx + x * 1 + 1x + 1 * 1',
                        'x ^ (1 + 1) + x * 1 + 1x + 1 * 1',
                        'x ^ 2 + x * 1 + 1x + 1 * 1',
+                       'x ^ 2 + x + 1x + 1 * 1',
+                       'x ^ 2 + x + x + 1 * 1',
                        'x ^ 2 + (1 + 1)x + 1 * 1',
                        'x ^ 2 + 2x + 1 * 1',
                        'x ^ 2 + 2x + 1'],
@@ -68,23 +74,44 @@ class TestLeidenOefenopgave(TestCase):
                        'xx + x * -1 - 1x - 1 * -1',
                        'x ^ (1 + 1) + x * -1 - 1x - 1 * -1',
                        'x ^ 2 + x * -1 - 1x - 1 * -1',
-                       # FIXME: 'x ^ 2 + (-1 - 1)x - 1 * -1',
-                       # FIXME: 'x ^ 2 - 2x - 1 * -1',
-                       # FIXME: 'x ^ 2 - 2x - -1',
-                       # FIXME: 'x ^ 2 - 2x + 1',
+                       'x ^ 2 - x * 1 - 1x - 1 * -1',
+                       'x ^ 2 - x - 1x - 1 * -1',
+                       'x ^ 2 - x - x - 1 * -1',
+                       'x ^ 2 + (1 + 1) * -x - 1 * -1',
+                       'x ^ 2 + 2 * -x - 1 * -1',
+                       'x ^ 2 - 2x - 1 * -1',
+                       'x ^ 2 - 2x - -1',
+                       'x ^ 2 - 2x + 1',
                      ]]:
             self.assertRewrite(chain)
 
     def test_1_4_1(self):
-        self.assertRewrite(['x * -1 + 1x', '(-1 + 1)x', '0x',])  # FIXME: '0'])
+        self.assertRewrite(['x * -1 + 1x',
+                            '-x * 1 + 1x',
+                            '-x + 1x',
+                            '-x + x',
+                            '(-1 + 1)x',
+                            '0x',
+                            '0'])
 
     def test_1_4_2(self):
-        # FIXME: self.assertRewrite(['x * -1 - 1x', '(-1 + -1)x', '-2x'])
-        pass
+        self.assertRewrite(['x * -1 - 1x',
+                            '-x * 1 - 1x',
+                            '-x - 1x',
+                            '-x - x',
+                            '(1 + 1) * -x',
+                            '2 * -x',
+                            '-2x'])
 
     def test_1_4_3(self):
-        # FIXME: self.assertRewrite(['x * -1 + x * -1', '(-1 + -1)x', '-2x'])
-        pass
+        self.assertRewrite(['x * -1 + x * -1',
+                            '-x * 1 + x * -1',
+                            '-x + x * -1',
+                            '-x - x * 1',
+                            '-x - x',
+                            '(1 + 1) * -x',
+                            '2 * -x',
+                            '-2x'])
 
     def test_1_5(self):
         self.assertRewrite(['(2x + x)x', '(2 + 1)xx', '3xx',
@@ -113,18 +140,25 @@ class TestLeidenOefenopgave(TestCase):
     def test_3(self):
         pass
 
-    def test_4(self):
-        for exp, solution in [
-                ('2/15 + 1/4',      '8 / 60 + 15 / 60'),
-                ('8/60 + 15/60',    '(8 + 15) / 60'),
-                ('(8 + 15) / 60',   '23 / 60'),
-                ('2/7 - 4/11',      '22 / 77 - 28 / 77'),
-                ('22/77 - 28/77',  '(22 - 28) / 77'),
-                ('(22 - 28)/77',    '-6 / 77'),
-                # FIXME: ('(7/3) * (3/5)',   '7 / 5'),
-                # FIXME: ('(3/4) / (5/6)',   '9 / 10'),
-                # FIXME: ('1/4 * 1/x',       '1 / (4x)'),
-                # FIXME: ('(3/x^2) / (x/7)', '21 / x^3'),
-                # FIXME: ('1/x + 2/(x+1)',   '(3x + 1) / (x * (x + 1))'),
-                ]:
-            self.assertEqual(str(rewrite(exp)), solution)
+    def test_4_1(self):
+        self.assertRewrite(['2/15 + 1/4', '8 / 60 + 15 / 60', '(8 + 15) / 60',
+                            '23 / 60'])
+
+    def test_4_2(self):
+        self.assertRewrite(['2/7 - 4/11', '22 / 77 - 28 / 77',
+                            '(22 - 28) / 77', '-6 / 77'])
+
+    #def test_4_3(self):
+    #    self.assertRewrite(['(7/3) * (3/5)', '7 / 5'])
+
+    #def test_4_4(self):
+    #    self.assertRewrite(['(3/4) / (5/6)', '9 / 10'])
+
+    #def test_4_5(self):
+    #    self.assertRewrite(['1/4 * 1/x', '1 / (4x)'])
+
+    #def test_4_6(self):
+    #    self.assertRewrite(['(3/x^2) / (x/7)', '21 / x^3'])
+
+    #def test_4_7(self):
+    #    self.assertRewrite(['1/x + 2/(x+1)', '(3x + 1) / (x * (x + 1))'])

+ 34 - 0
tests/test_leiden_oefenopgave_v12.py

@@ -0,0 +1,34 @@
+from tests.rulestestcase import RulesTestCase as TestCase, rewrite
+
+
+class TestLeidenOefenopgaveV12(TestCase):
+    def test_1_e(self):
+        self.assertRewrite([
+            '-2(6x - 4) ^ 2 * x',
+            '-2(6x - 4)(6x - 4)x',
+            '(-2 * 6x - 2 * -4)(6x - 4)x',
+            '(-12x - 2 * -4)(6x - 4)x',
+            '(-12x - -8)(6x - 4)x',
+            '(-12x + 8)(6x - 4)x',
+            '(-12x * 6x - 12x * -4 + 8 * 6x + 8 * -4)x',
+            '(-72xx - 12x * -4 + 8 * 6x + 8 * -4)x',
+            '(-72 * x ^ (1 + 1) - 12x * -4 + 8 * 6x + 8 * -4)x',
+            '(-72 * x ^ 2 - 12x * -4 + 8 * 6x + 8 * -4)x',
+            '(-72 * x ^ 2 - -48x + 8 * 6x + 8 * -4)x',
+            '(-72 * x ^ 2 + 48x + 8 * 6x + 8 * -4)x',
+            '(-72 * x ^ 2 + 48x + 48x + 8 * -4)x',
+            '(-72 * x ^ 2 + (1 + 1) * 48x + 8 * -4)x',
+            '(-72 * x ^ 2 + 2 * 48x + 8 * -4)x',
+            '(-72 * x ^ 2 + 96x + 8 * -4)x',
+            '(-72 * x ^ 2 + 96x - 32)x',
+            'x(-72 * x ^ 2 + 96x) + x * -32',
+            'x * -72 * x ^ 2 + x * 96x + x * -32',
+            '-x * 72 * x ^ 2 + x * 96x + x * -32',
+            '-x * 72 * x ^ 2 + x ^ (1 + 1) * 96 + x * -32',
+            '-x * 72 * x ^ 2 + x ^ 2 * 96 + x * -32',
+            '-x * 72 * x ^ 2 + x ^ 2 * 96 - x * 32'])
+            # FIXME: '-x ^ (1 + 2) * 72 + x ^ 2 * 96 - x * 32',
+            # FIXME: '-x ^ 3 * 72 + x ^ 2 * 96 - x * 32',
+            # FIXME: '-72x ^ 3 + x ^ 2 * 96 - x * 32',
+            # FIXME: '-72x ^ 3 + 96x ^ 2 - x * 32',
+            # FIXME: '-72x ^ 3 + 96x ^ 2 - 32x'])

+ 22 - 18
tests/test_node.py

@@ -30,25 +30,17 @@ class TestNode(RulesTestCase):
         self.assertTrue(N('+', *self.l[:2]).is_op(OP_ADD))
         self.assertFalse(N('-', *self.l[:2]).is_op(OP_ADD))
 
-    def test_is_op_or_negated(self):
-        self.assertTrue(N('+', *self.l[:2]).is_op_or_negated(OP_ADD))
-        self.assertTrue(N('-', N('+', *self.l[:2])).is_op_or_negated(OP_ADD))
-        self.assertFalse(N('-', *self.l[:2]).is_op_or_negated(OP_ADD))
-        self.assertFalse(self.l[0].is_op_or_negated(OP_ADD))
-
     def test_is_leaf(self):
         self.assertTrue(L(2).is_leaf)
         self.assertFalse(N('+', *self.l[:2]).is_leaf)
 
-    def test_is_leaf_or_negated(self):
-        self.assertTrue(L(2).is_leaf_or_negated())
-        self.assertTrue(N('-', L(2)).is_leaf_or_negated())
-        self.assertFalse(N('+', *self.l[:2]).is_leaf_or_negated())
-        self.assertFalse(N('-', N('+', *self.l[:2])).is_leaf_or_negated())
-
     def test_is_power(self):
-        self.assertTrue(N('^', *self.l[:2]).is_power())
-        self.assertFalse(N('+', *self.l[:2]).is_power())
+        self.assertTrue(N('^', *self.l[2:]).is_power())
+        self.assertFalse(N('+', *self.l[2:]).is_power())
+
+    def test_is_power_exponent(self):
+        self.assertTrue(N('^', *self.l[2:]).is_power(5))
+        self.assertFalse(N('^', *self.l[2:]).is_power(2))
 
     def test_is_nary(self):
         self.assertTrue(N('+', *self.l[:2]).is_nary())
@@ -173,6 +165,13 @@ class TestNode(RulesTestCase):
         m0, m1 = tree('-5 * -3,-5 * 6')
         self.assertFalse(m0.equals(m1))
 
+    def test_equals_ignore_negation(self):
+        p0, p1 = tree('-(a + b), a + b')
+        self.assertTrue(p0.equals(p1, ignore_negation=True))
+
+        a0, a1 = tree('-a,a')
+        self.assertTrue(a0.equals(a1, ignore_negation=True))
+
     def test_scope___init__(self):
         self.assertEqual(self.scope.node, self.n)
         self.assertEqual(self.scope.nodes, [self.a, self.b, self.cd])
@@ -185,14 +184,14 @@ class TestNode(RulesTestCase):
         self.scope.remove(self.cd)
         self.assertEqual(self.scope.nodes, [self.a, self.b])
 
-    def test_scope_remove_replace(self):
-        self.scope.remove(self.cd, self.f)
-        self.assertEqual(self.scope.nodes, [self.a, self.b, self.f])
-
     def test_scope_remove_error(self):
         with self.assertRaises(ValueError):
             self.scope.remove(self.f)
 
+    def test_scope_replace(self):
+        self.scope.replace(self.cd, self.f)
+        self.assertEqual(self.scope.nodes, [self.a, self.b, self.f])
+
     def test_nary_node(self):
         a, b, c, d = tree('a,b,c,d')
 
@@ -205,3 +204,8 @@ class TestNode(RulesTestCase):
 
     def test_scope_as_nary_node(self):
         self.assertEqualNodes(self.scope.as_nary_node(), self.n)
+
+    def test_scope_as_nary_node_negated(self):
+        n = tree('-(a + b)')
+        self.assertEqualNodes(Scope(n).as_nary_node(), n)
+        self.assertEqualNodes(Scope(-n).as_nary_node(), -n)

+ 12 - 0
tests/test_parser.py

@@ -23,3 +23,15 @@ class TestParser(unittest.TestCase):
 
     def test_line(self):
         self.assertEqual(line(Parser, '4-a'), '4 - a')
+
+    def test_reset_after_failure(self):
+        parser = ParserWrapper(Parser)
+        parser.run(['-(3a+6b)'])
+        possibilities1 = parser.parser.possibilities
+        self.assertNotEqual(possibilities1, [])
+
+        parser.run(['5+2*6'])
+        possibilities2 = parser.parser.possibilities
+        self.assertNotEqual(possibilities2, [])
+
+        self.assertNotEqual(possibilities1, possibilities2)

+ 5 - 7
tests/test_possibilities.py

@@ -45,7 +45,7 @@ class TestPossibilities(unittest.TestCase):
         possibilities = parser.parser.possibilities
         self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
                     '<Possibility root="3 + 4" handler=add_numerics' \
-                    ' args=(3, 4, 3, 4)>')
+                    ' args=(<Scope of "3 + 4">, 3, 4)>')
 
     def test_multiple_runs(self):
         parser = ParserWrapper(Parser)
@@ -53,21 +53,19 @@ class TestPossibilities(unittest.TestCase):
         possibilities = parser.parser.possibilities
         self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
                     '<Possibility root="1 + 2" handler=add_numerics' \
-                    ' args=(1, 2, 1, 2)>')
+                    ' args=(<Scope of "1 + 2">, 1, 2)>')
 
-        # Keep previous possibilities (skip whitespace lines)
+        # Remove previous possibilities after second run() call.
         parser.run(['', ' '])
         possibilities = parser.parser.possibilities
-        self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
-                    '<Possibility root="1 + 2" handler=add_numerics' \
-                    ' args=(1, 2, 1, 2)>')
+        self.assertEqual(possibilities, [])
 
         # Overwrite previous possibilities with new ones
         parser.run(['3+4'])
         possibilities = parser.parser.possibilities
         self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
                     '<Possibility root="3 + 4" handler=add_numerics' \
-                    ' args=(3, 4, 3, 4)>')
+                    ' args=(<Scope of "3 + 4">, 3, 4)>')
 
     def test_filter_duplicates(self):
         a, b = ab = tree('a + b')

+ 17 - 0
tests/test_rules_goniometry.py

@@ -0,0 +1,17 @@
+from src.rules.goniometry import match_add_quadrants, add_quadrants
+from src.possibilities import Possibility as P
+from tests.rulestestcase import RulesTestCase, tree
+
+
+class TestRulesGoniometry(RulesTestCase):
+
+    def test_match_add_quadrants(self):
+        return
+        root = tree('sin(x) ^ 2 + cos(x) ^ 2')
+        possibilities = match_add_quadrants(root)
+        self.assertEqualPos(possibilities, [P(root, add_quadrants, ())])
+
+    def test_add_quadrants(self):
+        return
+        root = tree('sin(x) ^ 2 + cos(x) ^ 2')
+        self.assertEqual(add_quadrants(root, ()), 1)

+ 42 - 16
tests/test_rules_groups.py

@@ -1,4 +1,5 @@
 from src.rules.groups import match_combine_groups, combine_groups
+from src.node import Scope
 from src.possibilities import Possibility as P
 from tests.rulestestcase import RulesTestCase, tree
 
@@ -6,64 +7,88 @@ from tests.rulestestcase import RulesTestCase, tree
 class TestRulesGroups(RulesTestCase):
 
     def test_match_combine_groups_no_const(self):
-        a0, a1 = root = tree('a + a')
+        root, l1 = tree('a + a,1')
+        a0, a1 = root
+
+        possibilities = match_combine_groups(root)
+        self.assertEqualPos(possibilities,
+                [P(root, combine_groups, (Scope(root), l1, a0, a0,
+                                                       l1, a1, a1))])
+
+    def test_match_combine_groups_negation(self):
+        root, l1 = tree('-a + a,1')
+        a0, a1 = root
 
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
-                [P(root, combine_groups, (1, a0, a0, 1, a1, a1))])
+                [P(root, combine_groups, (Scope(root), -l1, +a0, a0,
+                                                       l1, a1, a1))])
 
     def test_match_combine_groups_single_const(self):
-        a0, mul = root = tree('a + 2a')
+        root, l1 = tree('a + 2a,1')
+        a0, mul = root
         l2, a1 = mul
 
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
-                [P(root, combine_groups, (1, a0, a0, l2, a1, mul))])
+                [P(root, combine_groups, (Scope(root), l1, a0, a0,
+                                                       l2, a1, mul))])
 
     def test_match_combine_groups_two_const(self):
         ((l2, a0), b), (l3, a1) = (m0, b), m1 = root = tree('2a + b + 3a')
 
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
-                [P(root, combine_groups, (l2, a0, m0, l3, a1, m1))])
+                [P(root, combine_groups, (Scope(root), l2, a0, m0,
+                                                       l3, a1, m1))])
 
     def test_match_combine_groups_n_const(self):
         ((l2, a0), (l3, a1)), (l4, a2) = (m0, m1), m2 = root = tree('2a+3a+4a')
 
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
-                [P(root, combine_groups, (l2, a0, m0, l3, a1, m1)),
-                 P(root, combine_groups, (l2, a0, m0, l4, a2, m2)),
-                 P(root, combine_groups, (l3, a1, m1, l4, a2, m2))])
+                [P(root, combine_groups, (Scope(root), l2, a0, m0,
+                                                       l3, a1, m1)),
+                 P(root, combine_groups, (Scope(root), l2, a0, m0,
+                                                       l4, a2, m2)),
+                 P(root, combine_groups, (Scope(root), l3, a1, m1,
+                                                       l4, a2, m2))])
 
     def test_match_combine_groups_identifier_group_no_const(self):
-        ab0, ab1 = root = tree('ab + ab')
+        root, l1 = tree('ab + ab,1')
+        ab0, ab1 = root
 
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
-                [P(root, combine_groups, (1, ab0, ab0, 1, ab1, ab1))])
+                [P(root, combine_groups, (Scope(root), l1, ab0, ab0,
+                                                       l1, ab1, ab1))])
 
     def test_match_combine_groups_identifier_group_single_const(self):
-        m0, m1 = root = tree('ab + 2ab')
+        root, l1 = tree('ab + 2ab,1')
+        m0, m1 = root
         (l2, a), b = m1
 
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
-                [P(root, combine_groups, (1, m0, m0, l2, a * b, m1))])
+                [P(root, combine_groups, (Scope(root), l1, m0, m0,
+                                                       l2, a * b, m1))])
 
     def test_match_combine_groups_identifier_group_unordered(self):
-        m0, m1 = root = tree('ab + ba')
+        root, l1 = tree('ab + ba,1')
+        m0, m1 = root
         b, a = m1
 
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
-                [P(root, combine_groups, (1, m0, m0, 1, b * a, m1))])
+                [P(root, combine_groups, (Scope(root), l1, m0, m0,
+                                                       l1, b * a, m1))])
 
     def test_combine_groups_simple(self):
         root, l1 = tree('a + a,1')
         a0, a1 = root
 
-        self.assertEqualNodes(combine_groups(root, (1, a0, a0, 1, a1, a1)),
+        self.assertEqualNodes(combine_groups(root,
+                              (Scope(root), l1, a0, a0, l1, a1, a1)),
                               (l1 + 1) * a0)
 
     def test_combine_groups_nary(self):
@@ -71,5 +96,6 @@ class TestRulesGroups(RulesTestCase):
         abb, ba = root
         ab, b = abb
 
-        self.assertEqualNodes(combine_groups(root, (1, ab, ab, 1, ba, ba)),
+        self.assertEqualNodes(combine_groups(root,
+                              (Scope(root), l1, ab, ab, l1, ba, ba)),
                               (l1 + 1) * ab + b)

+ 56 - 12
tests/test_rules_negation.py

@@ -1,12 +1,56 @@
-
-from src.rules.negation import match_negated_division, \
-        single_negated_division, double_negated_division
+from src.rules.negation import match_negated_factor, negated_factor, \
+        match_negate_polynome, negate_polynome, double_negation, \
+        match_negated_division, single_negated_division, \
+        double_negated_division
+from src.node import Scope
 from src.possibilities import Possibility as P
 from tests.rulestestcase import RulesTestCase, tree
 
 
 class TestRulesNegation(RulesTestCase):
 
+    def test_match_negated_factor(self):
+        a, b = root = tree('a * -b')
+        self.assertEqualPos(match_negated_factor(root),
+                [P(root, negated_factor, (Scope(root), b))])
+
+        (a, b), c = root = tree('a * -b * -c')
+        scope = Scope(root)
+        self.assertEqualPos(match_negated_factor(root),
+                [P(root, negated_factor, (scope, b)),
+                 P(root, negated_factor, (scope, c))])
+
+    def test_negated_factor(self):
+        a, b = root = tree('a * -b')
+        self.assertEqualNodes(negated_factor(root, (Scope(root), b)),
+                              -a * +b)
+
+        (a, b), c = root = tree('a * -b * -c')
+        self.assertEqualNodes(negated_factor(root, (Scope(root), b)),
+                              -a * +b * c)
+        self.assertEqualNodes(negated_factor(root, (Scope(root), c)),
+                              -a * b * +c)
+
+    def test_match_negate_polynome(self):
+        root = tree('--a')
+        self.assertEqualPos(match_negate_polynome(root),
+                [P(root, double_negation, ())])
+
+        root = tree('-(a + b)')
+        self.assertEqualPos(match_negate_polynome(root),
+                [P(root, negate_polynome, ())])
+
+    def test_double_negation(self):
+        root = tree('--a')
+        self.assertEqualNodes(double_negation(root, ()), ++root)
+
+    def test_negate_polynome(self):
+        a, b = root = tree('-(a + b)')
+        self.assertEqualNodes(negate_polynome(root, ()), -a + -b)
+
+        a, b = root = tree('-(a - b)')
+        self.assertEqualNodes(negate_polynome(root, ()), -a + -b)
+
     def test_match_negated_division_none(self):
         self.assertEqual(match_negated_division(tree('1 / 2')), [])
 
@@ -14,31 +58,31 @@ class TestRulesNegation(RulesTestCase):
         l1, l2 = root = tree('-1 / 2')
         possibilities = match_negated_division(root)
         self.assertEqualPos(possibilities,
-                [P(root, single_negated_division, (l1[0], l2))])
+                [P(root, single_negated_division, (+l1, l2))])
 
         l1, l2 = root = tree('1 / -2')
         possibilities = match_negated_division(root)
         self.assertEqualPos(possibilities,
-                [P(root, single_negated_division, (l1, l2[0]))])
+                [P(root, single_negated_division, (l1, +l2))])
 
     def test_match_negated_division_double(self):
         root = tree('-1 / -2')
 
         possibilities = match_negated_division(root)
         self.assertEqualPos(possibilities,
-                [P(root, double_negated_division, (root,))])
+                [P(root, double_negated_division, ())])
 
     def test_single_negated_division(self):
         l1, l2 = root = tree('-1 / 2')
-        self.assertEqualNodes(single_negated_division(root, (l1[0], l2)),
-                              -(l1[0] / l2))
+        self.assertEqualNodes(single_negated_division(root, (+l1, l2)),
+                              -(+l1 / l2))
 
         l1, l2 = root = tree('1 / -2')
-        self.assertEqualNodes(single_negated_division(root, (l1, l2[0])),
-                              -(l1 / l2[0]))
+        self.assertEqualNodes(single_negated_division(root, (l1, +l2)),
+                              -(l1 / +l2))
 
     def test_double_negated_division(self):
         l1, l2 = root = tree('-1 / -2')
 
-        self.assertEqualNodes(double_negated_division(root, (root,)),
-                              l1[0] / l2[0])
+        self.assertEqualNodes(double_negated_division(root, ()),
+                              +l1 / +l2)

+ 50 - 32
tests/test_rules_numerics.py

@@ -1,26 +1,40 @@
-from src.rules.numerics import add_numerics, match_divide_numerics, \
-        divide_numerics, match_multiply_numerics, multiply_numerics
+from src.rules.numerics import match_add_numerics, add_numerics, \
+        match_divide_numerics, divide_numerics, match_multiply_numerics, \
+        multiply_numerics
+from src.node import ExpressionLeaf as L, Scope
 from src.possibilities import Possibility as P
-from src.node import ExpressionLeaf as L
 from tests.rulestestcase import RulesTestCase, tree
 
 
 class TestRulesNumerics(RulesTestCase):
 
+    def test_match_add_numerics(self):
+        l1, l2 = root = tree('1 + 2')
+        possibilities = match_add_numerics(root)
+        self.assertEqualPos(possibilities,
+                [P(root, add_numerics, (Scope(root), l1, l2))])
+
+        (l1, b), l2 = root = tree('1 + b + 2')
+        possibilities = match_add_numerics(root)
+        self.assertEqualPos(possibilities,
+                [P(root, add_numerics, (Scope(root), l1, l2))])
+
     def test_add_numerics(self):
         l0, a, l1 = tree('1,a,2')
 
-        self.assertEqual(add_numerics(l0 + l1, (l0, l1, L(1), L(2))), 3)
-        self.assertEqual(add_numerics(l0 + a + l1, (l0, l1, L(1), L(2))),
-                         L(3) + a)
+        root = l0 + l1
+        self.assertEqual(add_numerics(root, (Scope(root), l0, l1)), 3)
+        root = l0 + a + l1
+        self.assertEqual(add_numerics(root, (Scope(root), l0, l1)), L(3) + a)
 
     def test_add_numerics_negations(self):
-        l0, a, l1 = tree('1,a,2')
+        l1, a, l2 = tree('1,a,2')
+        ml1, ml2 = -l1, -l2
 
-        self.assertEqual(add_numerics(-l0 + l1, (-l0, l1, -L(1), L(2))), 1)
-        self.assertEqual(add_numerics(l0 + -l1, (l0, -l1, L(1), -L(2))), -1)
-        self.assertEqual(add_numerics(l0 + a + -l1, (l0, -l1, L(1), -L(2))),
-                         L(-1) + a)
+        r = ml1 + l2
+        self.assertEqual(add_numerics(r, (Scope(r), ml1, l2)), 1)
+        r = l1 + ml2
+        self.assertEqual(add_numerics(r, (Scope(r), l1, ml2)), -1)
 
     def test_match_divide_numerics(self):
         a, b, i2, i3, i6, f1, f2, f3 = tree('a,b,2,3,6,1.0,2.0,3.0')
@@ -71,45 +85,49 @@ class TestRulesNumerics(RulesTestCase):
 
         root = i3 * i2
         self.assertEqual(match_multiply_numerics(root),
-                [P(root, multiply_numerics, (i3, i2, 3, 2))])
+                [P(root, multiply_numerics, (Scope(root), i3, i2))])
 
         root = f3 * i2
         self.assertEqual(match_multiply_numerics(root),
-                [P(root, multiply_numerics, (f3, i2, 3.0, 2))])
+                [P(root, multiply_numerics, (Scope(root), f3, i2))])
 
         root = i3 * f2
         self.assertEqual(match_multiply_numerics(root),
-                [P(root, multiply_numerics, (i3, f2, 3, 2.0))])
+                [P(root, multiply_numerics, (Scope(root), i3, f2))])
 
         root = f3 * f2
         self.assertEqual(match_multiply_numerics(root),
-                [P(root, multiply_numerics, (f3, f2, 3.0, 2.0))])
+                [P(root, multiply_numerics, (Scope(root), f3, f2))])
 
     def test_multiply_numerics(self):
         a, b, i2, i3, i6, f2, f3, f6 = tree('a,b,2,3,6,2.0,3.0,6.0')
 
-        self.assertEqual(multiply_numerics(i3 * i2, (i3, i2, 3, 2)), 6)
-        self.assertEqual(multiply_numerics(f3 * i2, (f3, i2, 3.0, 2)), 6.0)
-        self.assertEqual(multiply_numerics(i3 * f2, (i3, f2, 3, 2.0)), 6.0)
-        self.assertEqual(multiply_numerics(f3 * f2, (f3, f2, 3.0, 2.0)), 6.0)
+        root = i3 * i2
+        self.assertEqual(multiply_numerics(root, (Scope(root), i3, i2)), 6)
+        root = f3 * i2
+        self.assertEqual(multiply_numerics(root, (Scope(root), f3, i2)), 6.0)
+        root = i3 * f2
+        self.assertEqual(multiply_numerics(root, (Scope(root), i3, f2)), 6.0)
+        root = f3 * f2
+        self.assertEqual(multiply_numerics(root, (Scope(root), f3, f2)), 6.0)
 
-        self.assertEqualNodes(multiply_numerics(a * i3 * i2 * b, (i3, i2, 3, 2)),
-                              a * 6 * b)
+        root = a * i3 * i2 * b
+        self.assertEqualNodes(multiply_numerics(root,
+                              (Scope(root), i3, i2)), a * 6 * b)
 
     def test_multiply_numerics_negation(self):
         l1_neg, l2 = root = tree('-1 * 2')
-        self.assertEqualNodes(multiply_numerics(root, (l1_neg, l2, -1, 2)), -l2)
-
-        root, l6 = tree('1 - 2 * 3,6')
-        l1, neg = root
-        l2, l3 = mul = neg[0]
-        self.assertEqualNodes(multiply_numerics(mul, (l2, l3, 2, 3)), l6)
+        self.assertEqualNodes(multiply_numerics(root, (Scope(root), l1_neg,
+                                                      l2)), -l2)
 
-        l1, mul = root = tree('1 + -2 * 3')
+        root, l6 = tree('1 + -2 * 3,6')
+        l1, mul = root
         l2_neg, l3 = mul
-        self.assertEqualNodes(multiply_numerics(mul, (l2_neg, l3, -2, 3)), -l6)
+        self.assertEqualNodes(multiply_numerics(mul, (Scope(mul),
+                                                      l2_neg, l3)), -l6)
 
         root, l30 = tree('-5 * x ^ 2 - -15x - 5 * 6,30')
-        rest, mul_neg = root
-        l5_neg, l6 = mul = mul_neg[0]
-        self.assertEqualNodes(multiply_numerics(mul, (l5_neg, l6, 5, 6)), l30)
+        rest, mul = root
+        l5_neg, l6 = mul
+        self.assertEqualNodes(multiply_numerics(mul, (Scope(mul),
+                                                      l5_neg, l6)), -l30)

+ 0 - 112
tests/test_rules_poly.py

@@ -1,112 +0,0 @@
-from src.rules.poly import match_combine_polynomes, combine_polynomes
-from src.rules.numerics import add_numerics
-from src.possibilities import Possibility as P
-from tests.rulestestcase import RulesTestCase, tree
-
-
-class TestRulesPoly(RulesTestCase):
-
-    def test_identifiers_basic(self):
-        a1, a2 = root = tree('a+a')
-        possibilities = match_combine_polynomes(root)
-        self.assertEqualPos(possibilities,
-                [P(root, combine_polynomes, (a1, a2, 1, 1, 'a', 1))])
-
-    def test_identifiers_normal(self):
-        a1, a2 = root = tree('a+2a')
-        possibilities = match_combine_polynomes(root)
-        self.assertEqualPos(possibilities,
-                [P(root, combine_polynomes, (a1, a2, 1, 2, 'a', 1))])
-
-    def test_identifiers_reverse(self):
-        a1, a2 = root = tree('a+a*2')
-        possibilities = match_combine_polynomes(root)
-        self.assertEqualPos(possibilities,
-                [P(root, combine_polynomes, (a1, a2, 1, 2, a1, 1))])
-
-    def test_identifiers_exponent(self):
-        a1, a2 = root = tree('a2+a2')
-        possibilities = match_combine_polynomes(root)
-        self.assertEqualPos(possibilities,
-                [P(root, combine_polynomes, (a1, a2, 1, 1, 'a', 2))])
-
-    def test_identifiers_coeff_exponent_left(self):
-        a1, a2 = root = tree('2a3+a3')
-        possibilities = match_combine_polynomes(root)
-        self.assertEqualPos(possibilities,
-                [P(root, combine_polynomes, (a1, a2, 2, 1, 'a', 3))])
-
-    def test_identifiers_coeff_exponent_both(self):
-        a1, a2 = root = tree('2a3+2a3')
-        possibilities = match_combine_polynomes(root)
-        self.assertEqualPos(possibilities,
-                [P(root, combine_polynomes, (a1, a2, 2, 2, 'a', 3))])
-
-    def test_basic_subexpressions(self):
-        a_b, c, d = tree('a+b,c,d')
-        left, right = root = tree('(a+b)^d + (a+b)^d')
-
-        self.assertEqual(left, right)
-        possibilities = match_combine_polynomes(root)
-        self.assertEqualPos(possibilities,
-                [P(root, combine_polynomes, (left, right, 1, 1, a_b, d))])
-
-        left, right = root = tree('5(a+b)^d + 7(a+b)^d')
-
-        possibilities = match_combine_polynomes(root)
-        self.assertEqualPos(possibilities,
-                [P(root, combine_polynomes, (left, right, 5, 7, a_b, d))])
-
-        # TODO: Move to other strategy
-        #left, right = root = tree('c(a+b)^d + c(a+b)^d')
-
-        #self.assertEqual(left, right)
-        #possibilities = match_combine_polynomes(root)
-        #self.assertEqualPos(possibilities,
-        #        [P(root, combine_polynomes, (left, right, c, c, a_b, d))])
-
-    def test_match_add_numerics(self):
-        l0, l1, l2 = tree('0,1,2')
-        root = l0 + l1 + l2
-
-        possibilities = match_combine_polynomes(root)
-        self.assertEqualPos(possibilities,
-                [P(root, add_numerics, (l0, l1, l0, l1)),
-                 P(root, add_numerics, (l0, l2, l0, l2)),
-                 P(root, add_numerics, (l1, l2, l1, l2))])
-
-    def test_match_add_numerics_explicit_powers(self):
-        l0, l1, l2 = tree('0^1,1*1,1*2^1')
-        root = l0 + l1 + l2
-
-        possibilities = match_combine_polynomes(root)
-        self.assertEqualPos(possibilities,
-                [P(root, add_numerics, (l0, l1, l0[0], l1[1])),
-                 P(root, add_numerics, (l0, l2, l0[0], l2[1][0])),
-                 P(root, add_numerics, (l1, l2, l1[1], l2[1][0]))])
-
-    def test_combine_polynomes(self):
-        # 2a + 3a -> (2 + 3) * a
-        l0, a, l1, l2 = tree('2,a,3,1')
-        root = l0 * a + l1 * a
-        left, right = root
-        replacement = combine_polynomes(root, (left, right, l0, l1, a, 1))
-        self.assertEqualNodes(replacement, (l0 + l1) * a)
-
-        # a + 3a -> (1 + 3) * a
-        root = a + l1 * a
-        left, right = root
-        replacement = combine_polynomes(root, (left, right, l2, l1, a, 1))
-        self.assertEqualNodes(replacement, (l2 + l1) * a)
-
-        # 2a + a -> (2 + 1) * a
-        root = l0 * a + a
-        left, right = root
-        replacement = combine_polynomes(root, (left, right, l0, l2, a, 1))
-        self.assertEqualNodes(replacement, (l0 + 1) * a)
-
-        # a + a -> (1 + 1) * a
-        root = a + a
-        left, right = root
-        replacement = combine_polynomes(root, (left, right, l2, l2, a, 1))
-        self.assertEqualNodes(replacement, (l2 + 1) * a)

+ 41 - 13
tests/test_rules_powers.py

@@ -3,9 +3,10 @@ from src.rules.powers import match_add_exponents, add_exponents, \
         match_multiply_exponents, multiply_exponents, \
         match_duplicate_exponent, duplicate_exponent, \
         match_remove_negative_exponent, remove_negative_exponent, \
-        match_exponent_to_root, exponent_to_root
+        match_exponent_to_root, exponent_to_root, \
+        match_constant_exponent, remove_power_of_zero, remove_power_of_one
+from src.node import Scope, ExpressionNode as N
 from src.possibilities import Possibility as P
-from src.node import ExpressionNode as N
 from tests.rulestestcase import RulesTestCase, tree
 
 
@@ -17,7 +18,7 @@ class TestRulesPowers(RulesTestCase):
 
         possibilities = match_add_exponents(root)
         self.assertEqualPos(possibilities,
-                [P(root, add_exponents, (n0, n1, a, p, q))])
+                [P(root, add_exponents, (Scope(root), n0, n1, a, p, q))])
 
     def test_match_add_exponents_ternary(self):
         a, p, q, r = tree('a,p,q,r')
@@ -25,9 +26,9 @@ class TestRulesPowers(RulesTestCase):
 
         possibilities = match_add_exponents(root)
         self.assertEqualPos(possibilities,
-                [P(root, add_exponents, (n0, n1, a, p, q)),
-                 P(root, add_exponents, (n0, n2, a, p, r)),
-                 P(root, add_exponents, (n1, n2, a, q, r))])
+                [P(root, add_exponents, (Scope(root), n0, n1, a, p, q)),
+                 P(root, add_exponents, (Scope(root), n0, n2, a, p, r)),
+                 P(root, add_exponents, (Scope(root), n1, n2, a, q, r))])
 
     def test_match_add_exponents_multiple_identifiers(self):
         a, b, p, q = tree('a,b,p,q')
@@ -35,8 +36,16 @@ class TestRulesPowers(RulesTestCase):
 
         possibilities = match_add_exponents(root)
         self.assertEqualPos(possibilities,
-                [P(root, add_exponents, (a0, a1, a, p, q)),
-                 P(root, add_exponents, (b0, b1, b, p, q))])
+                [P(root, add_exponents, (Scope(root), a0, a1, a, p, q)),
+                 P(root, add_exponents, (Scope(root), b0, b1, b, p, q))])
+
+    def test_match_add_exponents_nary_multiplication(self):
+        a, p, q = tree('a,p,q')
+        (n0, l1), n1 = root = a ** p * 2 * a ** q
+
+        possibilities = match_add_exponents(root)
+        self.assertEqualPos(possibilities,
+                [P(root, add_exponents, (Scope(root), n0, n1, a, p, q))])
 
     def test_match_subtract_exponents_powers(self):
         a, p, q = tree('a,p,q')
@@ -84,7 +93,7 @@ class TestRulesPowers(RulesTestCase):
 
         possibilities = match_remove_negative_exponent(root)
         self.assertEqualPos(possibilities,
-                [P(root, remove_negative_exponent, (a, p))])
+                [P(root, remove_negative_exponent, (a, -p))])
 
     def test_match_exponent_to_root(self):
         a, n, m, l1 = tree('a,n,m,1')
@@ -103,7 +112,8 @@ class TestRulesPowers(RulesTestCase):
         a, p, q = tree('a,p,q')
         n0, n1 = root = a ** p * a ** q
 
-        self.assertEqualNodes(add_exponents(root, (n0, n1, a, p, q)), a ** (p + q))
+        self.assertEqualNodes(add_exponents(root,
+                              (Scope(root), n0, n1, a, p, q)), a ** (p + q))
 
     def test_subtract_exponents(self):
         a, p, q = tree('a,p,q')
@@ -131,11 +141,11 @@ class TestRulesPowers(RulesTestCase):
                               a ** p * b ** p * c ** p)
 
     def test_remove_negative_exponent(self):
-        a, p, l1 = tree('a,p,1')
-        root = a ** -p
+        a, p, l1 = tree('a,-p,1')
+        root = a ** p
 
         self.assertEqualNodes(remove_negative_exponent(root, (a, p)),
-                              l1 / a ** p)
+                              l1 / a ** +p)
 
     def test_exponent_to_root(self):
         a, n, m, l1 = tree('a,n,m,1')
@@ -146,3 +156,21 @@ class TestRulesPowers(RulesTestCase):
 
         self.assertEqualNodes(exponent_to_root(root, (a, l1, m)),
                               N('sqrt', a, m))
+
+    def test_match_constant_exponent(self):
+        a0, a1, a2 = tree('a0,a1,a2')
+
+        self.assertEqualPos(match_constant_exponent(a0),
+                            [P(a0, remove_power_of_zero, ())])
+
+        self.assertEqualPos(match_constant_exponent(a1),
+                            [P(a1, remove_power_of_one, ())])
+
+        self.assertEqualPos(match_constant_exponent(a2), [])
+
+    def test_remove_power_of_zero(self):
+        self.assertEqual(remove_power_of_zero(tree('a0'), ()), 1)
+
+    def test_remove_power_of_one(self):
+        a1 = tree('a1')
+        self.assertEqual(remove_power_of_one(a1, ()), a1[0])