Parcourir la source

Merge branch 'master' of kompiler.org:trs

Sander Mathijs van Veen il y a 14 ans
Parent
commit
2bc6587617

+ 56 - 39
TODO

@@ -1,48 +1,12 @@
+# vim: set fileencoding=utf-8 :
+
  - Fix BisonSyntaxError location tracking.
 
  - Sort polynom by its exponents?
 
- - No possibilities found for:
-   >>> a2b3 + a2b3
-   a ^ 2 * b ^ 3 + a ^ 2 * b ^ 3
-
- - 2 + 3 + 4 rewrites to 5 instead of 5 + 4
-   -> the problem is that the 'root' of the application is actually a subtree
-   of the entire expression. This means that the parent of each possibility
-   root (or 'subtree') must me stored to be able to replace the subtree.
-
  - MESSAGES needs to be expanded.
 
- - rewrite match_combine_polynomes to an even more generic form:
-   match_combine_factors.
-
- - "--ab + c" has no rewrite possibility. The graph of "--ab + c" is also
-   not valid:
-
-     -
-     │
-      +
-    ╭─┴╮
-    *  c
-   ╭┴╮
-   - b
-   │
-   a
-
- - The following expression gives a cycle in the possibilities:
-
-   >>> ab + ba
-   possibilities:
-     Group "ab" is multiplied by 1 and 1, combine them.
-   >>> (1 + 1) * ab
-   (1 + 1)ab
-   possibilities:
-     Combine the constants 1 and 1.
-     Group "1" is multiplied by 1 and 1, combine them.
-     Expand a(1 + 1).
-     Expand b(1 + 1).
-
- - Fix division by zero caused by "0/0".
+ - Fix division by zero caused by "0/0": Catch exception in front-end
 
 smvv@multivac ~/work/trs $ printf "a/0\n??" | ./main.py
 Traceback (most recent call last):
@@ -84,3 +48,56 @@ smvv@multivac ~/work/trs $ printf "0/1\n??" | ./main.py
 <Possibility root="0 / 1" handler=divide_numerics args=(0, 1)>
 Division of 0 by 1 reduces to 0.
 Division of 0 by 1 reduces to 0.
+
+ - Fractions constant rewrite rules.
+
+ - >>> (sin x) ^ 2 + (cos x) ^ 2
+   sin(x) ^ 2 + cos(x) ^ 2
+   >>> sin(x) ^ 2 + cos(x) ^ 2
+   sin(x ^ 2) + cos(x ^ 2)
+
+ - ExpressionNode.equals() werkend maken voor alle cases (negatie).
+
+ - validation: preorder traversal implementatie vergelijken met andere
+               implementaties.
+
+ - Fix the following loop using strategy (reduce_fraction_constants):
+   >>> 2 / 7 - 4 / 11
+   2 / 7 - 4 / 11
+   >>> @
+   22 / 77 - 28 / 77
+   >>> @
+   2 / 7 - 28 / 77
+   >>> @
+   2 / 7 + 4 / 11
+
+ - Cancel terms before multiplying constants: (3 * ...) / (3 * ...) -> ... / ...
+    >>> (7/3)*(3/5)
+    7 / 3 * (3 / 5)
+    >>> ??
+    Expand fraction with nominator greater than denominator 7 / 3 to an integer
+    plus a fraction.
+    Multiply fractions 7 / 3 and 3 / 5.
+    >>> @
+    7 * 3 / (3 * 5)
+    >>> ?
+    Multiply constant 7 with 3.
+    >>> @
+    21 / (3 * 5)
+    >>> @
+    21 / 15
+    >>> @
+    7 / 5
+
+ - filter_duplicates does not seem to work anymore...
+
+ - Fix error while parsing unicode PI:
+   >>> sin(1/2 * pi)
+   sin(1 / 2 * π)
+   >>> @
+   unknown char � ignored.
+   unknown char � ignored.
+   ERROR: 41.7-41.8: "syntax error, unexpected TIMES" near "*".
+   ERROR: 41.14-41.15: "syntax error, unexpected RPAREN" near ")".
+
+ - No matches for sin(pi), sin(2pi), sin(4pi), etc...

+ 1 - 1
external/graph_drawing

@@ -1 +1 @@
-Subproject commit 11940973bdfef9432438b054c65b28af2eb97d0c
+Subproject commit bb3c5d23dc2a15e0e634f72b1ca48e5b22817642

+ 148 - 65
src/node.py

@@ -9,6 +9,8 @@ from graph_drawing.graph import generate_graph
 from graph_drawing.line import generate_line
 from graph_drawing.node import Node, Leaf
 
+from unicode_math import PI as u_PI
+
 
 TYPE_OPERATOR = 1
 TYPE_IDENTIFIER = 2
@@ -29,9 +31,24 @@ 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
+
+# Special identifierd
+PI = 'pi'
 
 
 TYPE_MAP = {
@@ -41,17 +58,43 @@ TYPE_MAP = {
         }
 
 OP_MAP = {
+        ',': OP_COMMA,
         '+': OP_ADD,
-        # Either substraction or negation. Skip the operator sign in 'x' (= 2).
-        '-': lambda x: OP_SUB if len(x) > 2 else OP_NEG,
+        '-': 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: 'FUNCTION',
+        OP_SIN: 'FUNCTION',
+        OP_COS: 'FUNCTION',
+        OP_TAN: 'FUNCTION',
+        OP_INT: 'FUNCTION',
+        OP_SOLVE: 'FUNCTION',
+        OP_EQ: 'EQ',
+        OP_POSSIBILITIES: 'POSSIBILITIES',
+        OP_HINT: 'HINT',
+        OP_REWRITE_ALL: 'REWRITE_ALL',
+        OP_REWRITE: 'REWRITE',
         }
 
 
@@ -60,6 +103,10 @@ def to_expression(obj):
 
 
 class ExpressionBase(object):
+
+    def __init__(self, *args, **kwargs):
+        self.negated = 0
+
     def clone(self):
         return copy.deepcopy(self)
 
@@ -86,16 +133,11 @@ class ExpressionBase(object):
         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
+                self_value = '-' * (self.negated & 1) + str(self.value)
+                other_value = '-' * (other.negated & 1) + str(other.value)
+
+                return self_value < other_value
 
-        if self.is_op(OP_NEG) and self[0].is_leaf:
-            if other.is_leaf:
-                # Both are leafs, string compare the value.
-                return ('-' + str(self.value)) < str(other.value)
-            if other.is_op(OP_NEG) and other[0].is_leaf:
-                return ('-' + str(self.value)) < ('-' + str(other.value))
             # Self is a leaf, thus has less value than an expression node.
             return True
 
@@ -113,26 +155,11 @@ class ExpressionBase(object):
     def is_op(self, op):
         return not self.is_leaf and self.op == op
 
-    def is_op_or_negated(self, op):
-        if self.is_leaf:
+    def is_power(self, exponent=None):
+        if self.is_leaf or self.op != OP_POW:
             return False
 
-        if self.op == OP_NEG:
-            return self[0].is_op(op)
-
-        return self.op == op
-
-    def is_leaf_or_negated(self):
-        if  self.is_leaf:
-            return True
-
-        if self.is_op(OP_NEG):
-            return self[0].is_leaf
-
-        return False
-
-    def is_power(self):
-        return not self.is_leaf and self.op == OP_POW
+        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]
@@ -164,8 +191,18 @@ class ExpressionBase(object):
     def __pow__(self, other):
         return ExpressionNode('^', self, to_expression(other))
 
-    def __neg__(self):
-        return ExpressionNode('-', self)
+    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):
+        """Negate the node n times."""
+        return negate(self, self.negated + n)
 
 
 class ExpressionNode(Node, ExpressionBase):
@@ -174,9 +211,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)
 
@@ -184,10 +218,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
@@ -226,8 +258,10 @@ class ExpressionNode(Node, ExpressionBase):
             return (ExpressionLeaf(1), self[0], self[1])
 
         # rule: -r -> (1, r, 1)
-        if self.is_op(OP_NEG):
-            return (ExpressionLeaf(1), -self[0], ExpressionLeaf(1))
+        # rule: --r -> (1, r, 1)
+        # rule: ---r -> (1, r, 1)
+        if self.negated:
+            return (ExpressionLeaf(1), self, ExpressionLeaf(1))
 
         if self.op != OP_MUL:
             return
@@ -257,7 +291,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.
@@ -268,18 +302,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
 
@@ -303,13 +333,15 @@ 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):
     def __init__(self, *args, **kwargs):
         super(ExpressionLeaf, self).__init__(*args, **kwargs)
-
         self.type = TYPE_MAP[type(args[0])]
 
     def __eq__(self, other):
@@ -319,16 +351,42 @@ 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 self.negated == other.negated and self.type == other.type \
+               and self.value == other.value
+
+    def __str__(self):
+        val = str(self.value)
+
+        # Replace PI leaf by the Greek character
+        if val == PI:
+            val = u_PI
+
+        return '-' * self.negated + val
 
-        return other.type == self.type and self.value == other.value
+    def __repr__(self):
+        return str(self)
 
-    def equals(self, other):
+    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):
         """
@@ -339,6 +397,11 @@ class ExpressionLeaf(Leaf, ExpressionBase):
         # rule: 1 * r ^ 1 -> (1, r, 1)
         return (ExpressionLeaf(1), self, ExpressionLeaf(1))
 
+    def actual_value(self):
+        assert self.is_numeric()
+
+        return (1 - 2 * (self.negated & 1)) * self.value
+
 
 class Scope(object):
 
@@ -358,7 +421,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:
@@ -371,8 +441,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]
 
@@ -381,8 +451,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):
@@ -409,3 +482,13 @@ def get_scope(node):
             scope.append(child)
 
     return scope
+
+
+def negate(node, n=1):
+    """Negate the given node n times."""
+    assert n >= 0
+
+    new_node = node.clone()
+    new_node.negated = n
+
+    return new_node

+ 113 - 68
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,9 +14,11 @@ 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
+from node import ExpressionNode as Node, ExpressionLeaf as Leaf, OP_MAP, \
+        TOKEN_MAP, TYPE_OPERATOR, OP_COMMA, OP_NEG, OP_MUL, OP_DIV, Scope, PI
 from rules import RULES
-from possibilities import filter_duplicates, pick_suggestion, apply_suggestion
+from strategy import pick_suggestion
+from possibilities import filter_duplicates, apply_suggestion
 
 import Queue
 
@@ -44,6 +44,10 @@ class Parser(BisonParser):
     docstrings. Scanner rules are in the 'lexscript' attribute.
     """
 
+    # Words to be ignored by preprocessor
+    words = zip(*filter(lambda (s, op): TOKEN_MAP[op] == 'FUNCTION', \
+                        OP_MAP.iteritems()))[0] + ('raise', 'graph', PI)
+
     # Output directory of generated pybison files, including a trailing slash.
     buildDirectory = PYBISON_BUILD + '/'
 
@@ -52,10 +56,9 @@ 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', 'FUNCTION'] \
+             + filter(lambda t: t != 'FUNCTION', TOKEN_MAP.values())
 
     # ------------------------------
     # precedences
@@ -64,8 +67,11 @@ class Parser(BisonParser):
         ('left', ('COMMA', )),
         ('left', ('MINUS', 'PLUS')),
         ('left', ('TIMES', 'DIVIDE')),
+        ('left', ('EQ', )),
         ('left', ('NEG', )),
         ('right', ('POW', )),
+        ('right', ('FUNCTION', )),
+        #('right', ('SIN', 'COS', 'TAN', 'SOLVE', 'INT', 'SQRT')),
         )
 
     interactive = 0
@@ -74,13 +80,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):
@@ -106,13 +119,13 @@ class Parser(BisonParser):
 
     def hook_read_before(self):
         if self.possibilities:
-            if self.interactive:  # pragma: nocover
+            if self.verbose:  # pragma: nocover
                 print 'possibilities:'
 
             items = filter_duplicates(self.possibilities)
             self.last_possibilities = self.possibilities
 
-            if self.interactive:  # pragma: nocover
+            if self.verbose:  # pragma: nocover
                 print '  ' + '\n  '.join(map(str, items))
 
     def hook_read_after(self, data):
@@ -150,7 +163,7 @@ class Parser(BisonParser):
             left, right = filter(None, match.groups())
 
             # Filter words (otherwise they will be preprocessed as well)
-            if left + right in ['graph', 'raise']:
+            if left + right in Parser.words:
                 return left + right
 
             # If all characters on the right are numbers. e.g. "a4", the
@@ -163,6 +176,9 @@ class Parser(BisonParser):
             # match: ab | abc | abcd (where left = "a")
             return '*'.join([left] + list(right))
 
+        if self.verbose:  # pragma: nocover
+            data_before = data
+
         # Iteratively replace all matches.
         while True:
             data_after = re.sub(pattern, preprocess_data, data)
@@ -170,41 +186,32 @@ class Parser(BisonParser):
             if data == data_after:
                 break
 
-            if self.verbose:  # pragma: nocover
-                print 'hook_read_after() modified the input data:'
-                print 'before:', data.replace('\n', '\\n')
-                print 'after :', data_after.replace('\n', '\\n')
-
             data = data_after
 
+        if self.verbose and data_before != data_after:  # pragma: nocover
+            print 'hook_read_after() modified the input data:'
+            print 'before:', repr(data_before)
+            print 'after :', repr(data_after)
+
         return data
 
     def hook_handler(self, target, option, names, values, retval):
-        if target in ['exp', 'line', 'input'] or not retval \
-                or retval.type != TYPE_OPERATOR:
+        if target in ['exp', 'line', 'input'] or not retval:
             return retval
 
-        if self.subtree_map:
-            # 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.op not in RULES:
+        if not retval.negated and retval.type != TYPE_OPERATOR:
             return retval
 
-        for handler in RULES[retval.op]:
-            possibilities = handler(retval)
+        if retval.type == TYPE_OPERATOR and retval.op in RULES:
+            handlers = RULES[retval.op]
+        else:
+            handlers = []
 
-            # 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
+        if retval.negated:
+            handlers = RULES[OP_NEG]
 
+        for handler in handlers:
+            possibilities = handler(retval)
             self.possibilities.extend(possibilities)
 
         return retval
@@ -213,7 +220,8 @@ class Parser(BisonParser):
         print pick_suggestion(self.last_possibilities)
 
     def display_possibilities(self):
-        print '\n'.join(map(str, self.last_possibilities))
+        if self.last_possibilities:
+            print '\n'.join(map(str, self.last_possibilities))
 
     def rewrite(self):
         suggestion = pick_suggestion(self.last_possibilities)
@@ -224,8 +232,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
@@ -254,6 +261,7 @@ class Parser(BisonParser):
         """
         input :
               | input line
+              | input REWRITE NEWLINE
         """
         if option == 1:
             # Interactive mode is enabled if the term rewriting system is used
@@ -264,6 +272,10 @@ class Parser(BisonParser):
 
             return values[1]
 
+        if option == 2:  # rule: input REWRITE NEWLINE
+            self.root_node = self.rewrite()
+            return self.root_node
+
     def on_line(self, target, option, names, values):
         """
         line : NEWLINE
@@ -271,11 +283,17 @@ class Parser(BisonParser):
              | debug NEWLINE
              | HINT NEWLINE
              | POSSIBILITIES NEWLINE
-             | REWRITE NEWLINE
              | RAISE NEWLINE
         """
         if option == 1:  # rule: EXP NEWLINE
             self.root_node = values[0]
+
+            # Clear list of last possibilities when current expression has no
+            # possibilities. Otherwise, an invalid expression gets the last
+            # possibilities of a valid expression.
+            if not self.possibilities:
+                self.last_possibilities = []
+
             return values[0]
 
         if option == 2:  # rule: DEBUG NEWLINE
@@ -290,11 +308,7 @@ class Parser(BisonParser):
             self.display_possibilities()
             return
 
-        if option == 5:  # rule: REWRITE NEWLINE
-            self.root_node = self.rewrite()
-            return self.root_node
-
-        if option == 6:
+        if option == 5:
             raise RuntimeError('on_line: exception raised')
 
     def on_debug(self, target, option, names, values):
@@ -340,10 +354,25 @@ class Parser(BisonParser):
     def on_unary(self, target, option, names, values):
         """
         unary : MINUS exp %prec NEG
+              | FUNCTION exp
         """
 
         if option == 0:  # rule: NEG exp
-            return Node('-', values[1])
+            node = values[1]
+            # Add negation to the left-most child
+            if node.is_leaf or (node.op != OP_MUL and node.op != OP_DIV):
+                node.negated += 1
+            else:
+                child = Scope(node)[0]
+                child.negated += 1
+
+            return node
+
+        if option == 1:  # rule: FUNCTION exp
+            if values[1].is_op(OP_COMMA):
+                return Node(values[0], *values[1])
+
+            return Node(*values)
 
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
                                % (option, target))  # pragma: nocover
@@ -354,19 +383,27 @@ class Parser(BisonParser):
                | exp TIMES exp
                | exp DIVIDE exp
                | exp POW exp
+               | exp EQ exp
                | exp MINUS exp
         """
 
-        if 0 <= option < 4:  # rule: exp {PLUS,TIMES,DIVIDES,POW} exp
+        if 0 <= option < 5:  # rule: exp {PLUS,TIMES,DIVIDES,POW,EQ} exp
             return Node(values[1], values[0], values[2])
 
-        if option == 4:  # rule: exp MINUS exp
-            # It is necessary to call the hook_handler here explicitly, since
-            # the minus operator is internally represented as two nodes (unary
-            # negation and binary plus).
-            node = Node('-', values[2])
-            node = self.hook_handler(target, option, names, values, node)
-            return Node('+', values[0], node)
+        if option == 5:  # rule: exp MINUS exp
+            node = values[2]
+
+            # Add negation to the left-most child
+            if node.is_leaf or (node.op != OP_MUL and node.op != OP_DIV):
+                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
@@ -382,6 +419,25 @@ class Parser(BisonParser):
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
                                % (option, target))  # pragma: nocover
 
+    # -----------------------------------------
+    # PI and operator tokens
+    # -----------------------------------------
+    operators = '"%s"%s{ returntoken(IDENTIFIER); }\n' \
+                % (PI, ' ' * (8 - len(PI)))
+    functions = []
+
+    for op_str, op in OP_MAP.iteritems():
+        if TOKEN_MAP[op] == 'FUNCTION':
+            functions.append(op_str)
+        else:
+            operators += '"%s"%s{ returntoken(%s); }\n' \
+                         % (op_str, ' ' * (8 - len(op_str)), TOKEN_MAP[op])
+
+    # Put all functions in a single regex
+    if functions:
+        operators += '("%s") { returntoken(FUNCTION); }\n' \
+                     % '"|"'.join(functions)
+
     # -----------------------------------------
     # raw lex script, verbatim here
     # -----------------------------------------
@@ -409,8 +465,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
@@ -421,19 +475,10 @@ 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); }

+ 23 - 11
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
@@ -51,16 +54,27 @@ def filter_duplicates(possibilities):
     return unique
 
 
-def pick_suggestion(possibilities):
-    if not possibilities:
-        return
+def find_parent_node(root, child):
+    nodes = [root]
+
+    while nodes:
+        node = nodes.pop()
+
+        while node:
 
-    # TODO: pick the best suggestion.
-    suggestion = 0
-    return possibilities[suggestion]
+            if node.type != TYPE_OPERATOR:
+                break
 
+            if child in node:
+                return node
 
-def apply_suggestion(root, subtree_map, suggestion):
+            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 +82,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 +96,5 @@ def apply_suggestion(root, subtree_map, suggestion):
     if parent_node:
         parent_node.substitute(suggestion.root, subtree)
         return root
+
     return subtree

+ 33 - 18
src/rules/__init__.py

@@ -1,26 +1,41 @@
-from ..node import OP_ADD, OP_MUL, OP_DIV, OP_POW, OP_NEG
-from .poly import match_combine_polynomes
+from ..node import OP_ADD, OP_MUL, OP_DIV, OP_POW, OP_NEG, OP_SIN, OP_COS, \
+        OP_TAN
 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_raised_fraction, match_remove_negative_exponent, \
+        match_exponent_to_root, match_extend_exponent, match_constant_exponent
+from .numerics import match_add_numerics, match_divide_numerics, \
+        match_multiply_numerics, match_multiply_zero, match_multiply_one, \
+        match_raise_numerics
 from .fractions import match_constant_division, match_add_constant_fractions, \
-        match_expand_and_add_fractions
-from .negation import match_negate_group, match_negated_division
+        match_expand_and_add_fractions, match_multiply_fractions, \
+        match_divide_fractions, match_equal_fraction_parts
+from .negation import match_negated_factor, match_negate_polynome, \
+        match_negated_division
+from .sort import match_sort_multiplicants
+from .goniometry import match_add_quadrants, match_negated_parameter, \
+        match_half_pi_subtraction, match_standard_radian
 
 RULES = {
-        OP_ADD: [match_add_constant_fractions, match_combine_polynomes, \
-                 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, \
-                 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_ADD: [match_add_numerics, match_add_constant_fractions,
+                 match_combine_groups, match_add_quadrants],
+        OP_MUL: [match_multiply_numerics, match_expand, match_add_exponents,
+                 match_expand_and_add_fractions, match_multiply_zero,
+                 match_negated_factor, match_multiply_one,
+                 match_sort_multiplicants, match_multiply_fractions],
+        OP_DIV: [match_subtract_exponents, match_divide_numerics,
+                 match_constant_division, match_divide_fractions, \
+                 match_negated_division, match_equal_fraction_parts],
+        OP_POW: [match_multiply_exponents, match_duplicate_exponent,
+                 match_raised_fraction, match_remove_negative_exponent,
+                 match_exponent_to_root, match_extend_exponent,
+                 match_constant_exponent, match_raise_numerics],
+        OP_NEG: [match_negate_polynome],
+        OP_SIN: [match_negated_parameter, match_half_pi_subtraction,
+                 match_standard_radian],
+        OP_COS: [match_negated_parameter, match_half_pi_subtraction,
+                 match_standard_radian],
+        OP_TAN: [match_standard_radian],
         }

+ 8 - 4
src/rules/factors.py

@@ -1,6 +1,6 @@
 from itertools import product, combinations
 
-from ..node import Scope, OP_ADD, OP_MUL, OP_NEG
+from ..node import Scope, OP_ADD, OP_MUL
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 
@@ -18,9 +18,13 @@ def match_expand(node):
     additions = []
 
     for n in Scope(node):
-        if n.is_leaf or n.is_op(OP_NEG) and n[0].is_leaf:
+        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)

+ 286 - 47
src/rules/fractions.py

@@ -1,7 +1,8 @@
-from itertools import combinations
+from itertools import combinations, product
 
-from .utils import least_common_multiple
-from ..node import ExpressionLeaf as L, Scope, OP_DIV, OP_ADD, OP_MUL, OP_NEG
+from .utils import least_common_multiple, partition
+from ..node import ExpressionLeaf as L, Scope, negate, OP_DIV, OP_ADD, \
+        OP_MUL, OP_POW, nary_node, negate
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 
@@ -44,7 +45,7 @@ def division_by_one(root, args):
     return args[0]
 
 
-MESSAGES[division_by_one] = _('Division of {1} by 1 reduces to {1}.')
+MESSAGES[division_by_one] = _('Division by 1 yields the nominator.')
 
 
 def division_of_zero(root, args):
@@ -64,38 +65,30 @@ def division_by_self(root, args):
     return L(1)
 
 
-MESSAGES[division_by_self] = _('Division of {1} by {1} reduces to 1.')
+MESSAGES[division_by_self] = _('Division of {1} by itself reduces to 1.')
 
 
 def match_add_constant_fractions(node):
     """
     1 / 2 + 3 / 4  ->  2 / 4 + 3 / 4  # Equalize denominators
+    2 / 2 - 3 / 4  ->  4 / 4 - 3 / 4
     2 / 4 + 3 / 4  ->  5 / 4          # Equal denominators, so nominators can
                                       # be added
-    2 / 2 - 3 / 4  ->  4 / 4 - 3 / 4  # Equalize denominators
-    2 / 4 - 3 / 4  ->  -1 / 4         # Equal denominators, so nominators can
-                                      # be subtracted
+    2 / 4 - 3 / 4  ->  -1 / 4
+    1 / 2 + 3 / 4  ->  4 / 8 + 6 / 8  # Equalize denominators by multiplying
+                                      # them with eachother
+
     """
     assert node.is_op(OP_ADD)
 
     p = []
+    scope = Scope(node)
 
-    def is_division(node):
-        return node.is_op(OP_DIV) or \
-                (node.is_op(OP_NEG) and node[0].is_op(OP_DIV))
-
-    fractions = filter(is_division, Scope(node))
+    fractions = filter(lambda node: node.is_op(OP_DIV), scope)
 
     for a, b in combinations(fractions, 2):
-        if a.is_op(OP_NEG):
-            na, da = a[0]
-        else:
-            na, da = a
-
-        if b.is_op(OP_NEG):
-            nb, db = b[0]
-        else:
-            nb, db = b
+        na, da = a
+        nb, db = b
 
         if da == db:
             # Equal denominators, add nominators to create a single fraction
@@ -105,7 +98,12 @@ def match_add_constant_fractions(node):
             # least common multiple of their denominators. Later, the
             # nominators will be added
             denom = least_common_multiple(da.value, db.value)
-            p.append(P(node, equalize_denominators, (a, b, denom)))
+            p.append(P(node, equalize_denominators, (scope, a, b, denom)))
+
+            # Also, add the (non-recommended) possibility to multiply the
+            # denominators
+            p.append(P(node, equalize_denominators, (scope, a, b,
+                                                     da.value * db.value)))
 
     return p
 
@@ -113,29 +111,28 @@ def match_add_constant_fractions(node):
 def equalize_denominators(root, args):
     """
     1 / 2 + 3 / 4  ->  2 / 4 + 3 / 4
+    1 / 2 - 3 / 4  ->  2 / 4 - 3 / 4
     a / 2 + b / 4  ->  2a / 4 + b / 4
     """
-    denom = args[2]
+    scope, denom = args[::3]
 
-    scope = Scope(root)
-
-    for fraction in args[:2]:
-        n, d = fraction[0] if fraction.is_op(OP_NEG) else fraction
+    for fraction in args[1:3]:
+        n, d = fraction
         mult = denom / d.value
 
         if mult != 1:
-            n = L(n.value * mult) if n.is_numeric() else L(mult) * n
-
-            if fraction.is_op(OP_NEG):
-                scope.remove(fraction, -(n / L(d.value * mult)))
+            if n.is_numeric():
+                nom = L(n.value * mult)
             else:
-                scope.remove(fraction, n / L(d.value * mult))
+                nom = L(mult) * n
+
+            scope.replace(fraction, negate(nom / L(d.value * mult), n.negated))
 
     return scope.as_nary_node()
 
 
-MESSAGES[equalize_denominators] = _('Equalize the denominators of division'
-    ' of {1} by {2}.')
+MESSAGES[equalize_denominators] = _('Equalize the denominators of divisions'
+    ' {2} and {3} to {4}.')
 
 
 def add_nominators(root, args):
@@ -147,21 +144,11 @@ def add_nominators(root, args):
     """
     # TODO: is 'add' Appropriate when rewriting to "(a + (-c)) / b"?
     ab, cb = args
-
-    if ab.is_op(OP_NEG):
-        a, b = ab[0]
-    else:
-        a, b = ab
-
-    if cb.is_op(OP_NEG):
-        c = -cb[0][0]
-    else:
-        c = cb[0]
-
+    a, b = ab
     scope = Scope(root)
 
     # Replace the left node with the new expression
-    scope.remove(ab, (a + c) / b)
+    scope.replace(ab, (a + cb[0].negate(cb.negated)) / b)
 
     # Remove the right node
     scope.remove(cb)
@@ -185,3 +172,255 @@ def match_expand_and_add_fractions(node):
     p = []
 
     return p
+
+
+def match_multiply_fractions(node):
+    """
+    a / b * (c / d)  ->  ac / (bd)
+    a * (b / c)      ->  ab / c
+    """
+    assert node.is_op(OP_MUL)
+
+    p = []
+    scope = Scope(node)
+    fractions, others = partition(lambda n: n.is_op(OP_DIV), scope)
+
+    for ab, cd in combinations(fractions, 2):
+        p.append(P(node, multiply_fractions, (scope, ab, cd)))
+
+    for a, bc in product(others, fractions):
+        p.append(P(node, multiply_with_fraction, (scope, a, bc)))
+
+    return p
+
+
+def multiply_fractions(root, args):
+    """
+    a / b * (c / d)  ->  ac / (bd)
+    """
+    scope, ab, cd = args
+    a, b = ab
+    c, d = cd
+
+    scope.replace(ab, a * c / (b * d))
+    scope.remove(cd)
+
+    return scope.as_nary_node()
+
+
+MESSAGES[multiply_fractions] = _('Multiply fractions {2} and {3}.')
+
+
+def multiply_with_fraction(root, args):
+    """
+    a * (b / c)  ->  ab / c
+    """
+    scope, a, bc = args
+    b, c = bc
+
+    scope.replace(a, a * b / c)
+    scope.remove(bc)
+
+    return scope.as_nary_node()
+
+
+MESSAGES[multiply_with_fraction] = _('Multiply {2} with fraction {3}.')
+
+
+def match_divide_fractions(node):
+    """
+    Reduce divisions of fractions to a single fraction.
+
+    Examples:
+    a / b / c        ->  a / (bc)
+    a / (b / c)      ->  ac / b
+    """
+    assert node.is_op(OP_DIV)
+
+    nom, denom = node
+    p = []
+
+    if nom.is_op(OP_DIV):
+        p.append(P(node, divide_fraction, tuple(nom) + (denom,)))
+
+    if denom.is_op(OP_DIV):
+        p.append(P(node, divide_by_fraction, (nom,) + tuple(denom)))
+
+    return p
+
+
+def divide_fraction(root, args):
+    """
+    a / b / c        ->  a / (bc)
+    """
+    a, b, c = args
+
+    return a / (b * c)
+
+
+MESSAGES[divide_fraction] = _('Move {3} to denominator of fraction {1} / {2}.')
+
+
+def divide_by_fraction(root, args):
+    """
+    a / (b / c)      ->  ac / b
+    """
+    a, b, c = args
+
+    return a * c / b
+
+
+MESSAGES[divide_by_fraction] = \
+        _('Move {3} to nominator of fraction {1} / {2}.')
+
+
+#def match_extract_divided_fractions(node):
+#    """
+#    Reduce divisions of fractions to a single fraction.
+#
+#    Examples:
+#    a / b / c        ->  a / bc
+#    a / (b / c)      ->  ac / b
+#    # IMPLICIT: a / b / (c / d)  ->*  ad / bd  ->  validation test!
+#    """
+#    assert node.is_op(OP_DIV)
+#
+#    nom, denom = node
+#    n_scope, d_scope = fraction_scopes(node)
+#    is_division = lambda n: n.is_op(OP_DIV)
+#    n_fractions, n_others = partition(is_division, n_scope)
+#    d_fractions, d_others = partition(is_division, d_scope)
+#
+#
+#    return []
+
+
+def fraction_scopes(node):
+    """
+    Get the multiplication scopes of the nominator and denominator of a
+    fraction.
+    """
+    assert node.is_op(OP_DIV)
+
+    nominator, denominator = node
+
+    if nominator.is_op(OP_MUL):
+        n_scope = list(Scope(nominator))
+    else:
+        n_scope = [nominator]
+
+    if denominator.is_op(OP_MUL):
+        d_scope = list(Scope(denominator))
+    else:
+        d_scope = [denominator]
+
+    return n_scope, d_scope
+
+
+def match_equal_fraction_parts(node):
+    """
+    Divide nominator and denominator by the same part.
+
+    Examples:
+    ab / (ac)  ->  b / c
+    ab / a     ->  b / 1
+    a / (ab)   ->  1 / b
+
+    If the same root appears in both nominator and denominator, extrct it so
+    that it can be reduced to a single power by power division rules.
+    a ^ p * b / a ^ q  ->  a ^ p / a ^ q * b / 1
+    a ^ p * b / a      ->  a ^ p / a * b / 1
+    a * b / a ^ q      ->  a / a ^ q * b / 1
+    """
+    assert node.is_op(OP_DIV)
+
+    nominator, denominator = node
+    n_scope, d_scope = fraction_scopes(node)
+    p = []
+
+    # Look for matching parts in scopes
+    for i, n in enumerate(n_scope):
+        for j, d in enumerate(d_scope):
+            if n.equals(d, ignore_negation=True):
+                p.append(P(node, divide_fraction_parts,
+                           (negate(n, 0), n_scope, d_scope, i, j)))
+
+            if n.is_op(OP_POW):
+                a = n[0]
+
+                if d == a or (d.is_op(OP_POW) and d[0] == a):
+                    # a ^ p * b / a  ->  a ^ p / a * b
+                    p.append(P(node, extract_divided_roots,
+                               (a, n_scope, d_scope, i, j)))
+            elif d.is_op(OP_POW) and n == d[0]:
+                # a * b / a ^ q  ->  a / a ^ q * b
+                p.append(P(node, extract_divided_roots,
+                           (d[0], n_scope, d_scope, i, j)))
+
+    return p
+
+
+def remove_from_scopes(n_scope, d_scope, i, j):
+    a_n, a_d = n_scope[i], d_scope[j]
+
+    del n_scope[i]
+    del d_scope[j]
+
+    if not n_scope:
+        # Last element of nominator scope, replace by 1
+        nom = L(1)
+    elif len(n_scope) == 1:
+        # Only one element left, no multiplication
+        nom = n_scope[0]
+    else:
+        # Still a multiplication
+        nom = nary_node('*', n_scope)
+
+    if not d_scope:
+        denom = L(1)
+    elif len(n_scope) == 1:
+        denom = d_scope[0]
+    else:
+        denom = nary_node('*', d_scope)
+
+    return a_n, a_d, nom, denom
+
+
+def divide_fraction_parts(root, args):
+    """
+    Divide nominator and denominator by the same part.
+
+    Examples:
+    ab / (ac)  ->  b / c
+    ab / a     ->  b / 1
+    a / (ab)   ->  1 / b
+    -ab / a     ->  -b / 1
+    """
+    a, n_scope, d_scope, i, j = args
+    n, d = root
+    a_n, a_d, nom, denom = remove_from_scopes(n_scope, d_scope, i, j)
+
+    # Move negation of removed part to nominator and denominator
+    return nom.negate(n.negated + a_n.negated) \
+           / denom.negate(d.negated + a_d.negated)
+
+
+MESSAGES[divide_fraction_parts] = \
+        _('Divide nominator and denominator in {0} by {1}.')
+
+
+def extract_divided_roots(root, args):
+    """
+    a ^ p * b / a ^ q  ->  a ^ p / a ^ q * b / 1
+    a ^ p * b / a      ->  a ^ p / a * b / 1
+    a * b / a ^ q      ->  a / a ^ q * b / 1
+    """
+    a, n_scope, d_scope, i, j = args
+    n, d = root
+    ap, aq, nom, denom = remove_from_scopes(n_scope, d_scope, i, j)
+
+    return ap / aq * nom.negate(n.negated) / denom.negate(d.negated)
+
+
+MESSAGES[extract_divided_roots] = \
+        _('Extract the root {1} from nominator and denominator in {0}.')

+ 175 - 0
src/rules/goniometry.py

@@ -0,0 +1,175 @@
+from .utils import is_fraction
+from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, OP_ADD, \
+        OP_POW, OP_MUL, OP_DIV, OP_SIN, OP_COS, OP_TAN, PI, TYPE_OPERATOR
+from ..possibilities import Possibility as P, MESSAGES
+from ..translate import _
+
+
+def sin(*args):
+    return N('sin', *args)
+
+
+def cos(*args):
+    return N('cos', *args)
+
+
+def tan(*args):
+    return N('tan', *args)
+
+
+def match_add_quadrants(node):
+    """
+    sin(t) ^ 2 + cos(t) ^ 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(t) ^ 2 + cos(t) ^ 2  ->  1
+    """
+    return L(1)
+
+
+MESSAGES[add_quadrants] = _('Add the sinus and cosinus quadrants to 1.')
+
+
+def match_negated_parameter(node):
+    """
+    sin(-t)  ->  -sin(t)
+    cos(-t)  ->  cos(t)
+    """
+    assert node.is_op(OP_SIN) or node.is_op(OP_COS)
+
+    t = node[0]
+
+    if t.negated:
+        if node.op == OP_SIN:
+            return [P(node, negated_sinus_parameter, (t,))]
+
+        return [P(node, negated_cosinus_parameter, (t,))]
+
+    return []
+
+
+def negated_sinus_parameter(root, args):
+    """
+    sin(-t)  ->  -sin(t)
+    """
+    return -sin(+args[0])
+
+
+MESSAGES[negated_sinus_parameter] = \
+        _('Bring the negation from the sinus parameter {1} to the outside.')
+
+
+def negated_cosinus_parameter(root, args):
+    """
+    cos(-t)  ->  cos(t)
+    """
+    return cos(+args[0])
+
+
+MESSAGES[negated_cosinus_parameter] = \
+        _('Remove the negation from the cosinus parameter {1}.')
+
+
+def match_half_pi_subtraction(node):
+    """
+    sin(pi / 2 - t)  ->  cos(t)
+    cos(pi / 2 - t)  ->  sin(t)
+    """
+    assert node.is_op(OP_SIN) or node.is_op(OP_COS)
+
+    if node[0].is_op(OP_ADD):
+        half_pi, t = node[0]
+
+        if half_pi == L(PI) / 2:
+            if node.op == OP_SIN:
+                return [P(node, half_pi_subtraction_sinus, (t,))]
+
+    return []
+
+
+def is_pi_frac(node, denominator):
+    """
+    Check if a node is a fraction of 1 multiplied with PI.
+
+    Example:
+    >>> print is_pi_frac(L(1) / 2 * L(PI), 2)
+    True
+    """
+    if not node.is_op(OP_MUL):
+        return False
+
+    frac, pi = node
+
+    if not frac.is_op(OP_DIV) or not pi.is_leaf or pi.value != PI:
+        return False
+
+    n, d = frac
+
+    return n == 1 and d == denominator
+
+
+def sqrt(value):
+    return N('sqrt', L(value))
+
+
+l0, l1, sq2, sq3 = L(0), L(1), sqrt(2), sqrt(3)
+half = l1 / 2
+
+CONSTANTS = {
+    OP_SIN: [l0, half, half * sq2, half * sq3, l1],
+    OP_COS: [l1, half * sq3, half * sq2, half, l0],
+    OP_TAN: [l0, l1 / 3 * sq3, l1, sq3]
+}
+
+
+def match_standard_radian(node):
+    """
+    Apply a direct constant calculation from the constants table.
+
+        | 0 | pi / 6    | pi / 4    | pi / 3    | pi / 2
+    ----+---+-----------+-----------+-----------+-------
+    sin | 0 | 1/2       | sqrt(2)/2 | sqrt(3)/2 | 1
+    cos | 1 | sqrt(3)/2 | sqrt(2)/2 | 1/2       | 0
+    tan | 0 | sqrt(3)/3 | 1         | sqrt(3)   | -
+    """
+    assert node.type == TYPE_OPERATOR and node.op in (OP_SIN, OP_COS, OP_TAN)
+
+    t = node[0]
+
+    if t == 0:
+        return [P(node, standard_radian, (node.op, 0))]
+
+    denoms = [6, 4, 3]
+
+    if node.op != OP_TAN:
+        denoms.append(2)
+
+    for i, denominator in enumerate(denoms):
+        if is_pi_frac(t, denominator):
+            return [P(node, standard_radian, (node.op, i + 1))]
+
+    return []
+
+
+def standard_radian(root, args):
+    op, column = args
+
+    return CONSTANTS[op][column].clone()
+
+
+MESSAGES[standard_radian] = _('Replace standard radian {0}.')

+ 26 - 23
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_NEG
+from ..node import ExpressionLeaf as Leaf, Scope, OP_ADD, OP_MUL, nary_node, \
+        negate
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 
@@ -18,50 +18,53 @@ def match_combine_groups(node):
     ab + 2ab  ->  3ab
     ab + ba   ->  2ab
     """
-    # TODO: handle OP_NEG nodes
     assert node.is_op(OP_ADD)
 
     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):
-                if sub_node.is_numeric() or (sub_node.is_op(OP_NEG)
-                                             and sub_node[0].is_numeric()):
-                    others = [scope[j] for j in range(i) + range(i + 1, l)]
+            for i, sub_node in enumerate(n_scope):
+                if sub_node.is_numeric():
+                    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)
@@ -70,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.')

+ 83 - 65
src/rules/negation.py

@@ -1,88 +1,108 @@
-from ..node import get_scope, nary_node, OP_NEG, 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
-    --ab                ->  ab
-    -(-ab + c)          ->  --ab - c
-    -(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.is_op(OP_NEG)
+    assert node.is_op(OP_MUL)
 
-    val = node[0]
+    p = []
+    scope = Scope(node)
 
-    if val.is_op(OP_NEG):
-        # --a
-        return [P(node, double_negation, (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 not val.is_leaf:
-        scope = get_scope(val)
+    return p
 
-        if not any(map(lambda n: n.is_op(OP_NEG), scope)):
-            return []
 
-        if val.is_op(OP_MUL):
-            # --ab
-            return [P(node, negate_polynome, (node, scope))]
+def negated_factor(root, args):
+    """
+    a * -b  ->  -ab
+    """
+    scope, factor = args
+    scope[0] = -scope[0]
+    scope.replace(factor, +factor)
 
-        elif val.is_op(OP_ADD):
-            # -(ab + c)   ->  -ab - c
-            # -(-ab + c)  ->  ab - c
-            return [P(node, negate_group, (node, scope))]
+    return scope.as_nary_node()
 
-    return []
 
+MESSAGES[negated_factor] = \
+        _('Bring negation of {2} to the outside of the multiplication.')
 
-def negate_polynome(root, args):
+
+def match_negate_polynome(node):
     """
-    # -a * -3c  ->  a * 3c
-    --a * 3c  ->  a * 3c
-    --ab      ->  ab
-    --abc     ->  abc
+    --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):
-        # XXX: validate this property!
-        if n.is_op(OP_NEG):
-            scope[i] = n[0]
-            return nary_node('*', scope)
+    p = []
 
-    raise RuntimeError('No negation node found in scope.')
+    if node.negated == 2:
+        # --a
+        p.append(P(node, double_negation, ()))
 
+    if node.is_op(OP_ADD):
+        # -(a + b)  ->  -a - b
+        p.append(P(node, negate_polynome, ()))
 
-MESSAGES[negate_polynome] = _('Apply negation to the polynome {1[0]}.')
+    return p
 
 
-def negate_group(root, args):
+def double_negation(root, args):
     """
-    -(-ab + ... + c)  ->  --ab + ... + -c
+    --a  ->  a
     """
-    node, scope = args
+    return root.reduce_negation(2)
 
-    # Negate each group
-    for i, n in enumerate(scope):
-        scope[i] = -n
 
-    return nary_node('+', scope)
+MESSAGES[double_negation] = _('Remove double negation in {0}.')
 
 
-MESSAGES[negate_group] = _('Apply negation to the subexpression {1[0]}.')
-
-
-def double_negation(root, args):
+def negate_polynome(root, args):
     """
-    --a  ->  a
+    -(a + b)  ->  -a - b
     """
-    node = args[0]
+    scope = Scope(root)
+
+    # Negate each group
+    for i, n in enumerate(scope):
+        scope[i] = -n
+
+    return +scope.as_nary_node()
 
-    return node[0][0]
 
+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):
@@ -92,33 +112,28 @@ def match_negated_division(node):
     assert node.is_op(OP_DIV)
 
     a, b = node
-    a_neg = a.is_op(OP_NEG)
-    b_neg = b.is_op(OP_NEG)
 
-    if a_neg and b_neg:
-        return [P(node, double_negated_division, (node,))]
-    elif a_neg:
-        return [P(node, single_negated_division, (a[0], b))]
-    elif b_neg:
-        return [P(node, single_negated_division, (a, b[0]))]
+    if a.negated and b.negated:
+        return [P(node, double_negated_division, ())]
+    elif b.negated:
+        return [P(node, single_negated_division, (a, +b))]
 
     return []
 
 
 def single_negated_division(root, args):
     """
-    -a / b  ->  -(a / b)
-    a / -b  ->  -(a / b)
+    a / -b  ->  -a / b
     """
     a, b = args
 
     # FIXME: "-a/b" results in "-(a/b)", which will cause a loop.
 
-    return -(a / b)
+    return -a / b
 
 
 MESSAGES[single_negated_division] = \
-        _('Bring negation outside of the division: -({1} / {2}).')
+        _('Bring negation outside of the division: -{1} / {2}.')
 
 
 def double_negated_division(root, args):
@@ -127,8 +142,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

+ 187 - 67
src/rules/numerics.py

@@ -1,11 +1,13 @@
 from itertools import combinations
 
-from ..node import ExpressionLeaf as Leaf, Scope, OP_DIV, OP_MUL, OP_NEG
+from .utils import greatest_common_divisor
+from ..node import ExpressionLeaf as Leaf, Scope, negate, OP_ADD, OP_DIV, \
+        OP_MUL, OP_POW
 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.
 
@@ -14,31 +16,57 @@ def add_numerics(root, args):
     2 + -3   ->  -1
     -2 + 3   ->  1
     -2 + -3  ->  -5
+    0 + 3    ->  3
+    0 + -3   ->  -3
+    """
+    assert node.is_op(OP_ADD)
+
+    p = []
+    scope = Scope(node)
+    numerics = []
+
+    for n in scope:
+        if n == 0:
+            p.append(P(node, remove_zero, (scope, n)))
+        elif n.is_numeric():
+            numerics.append(n)
+
+    for c0, c1 in combinations(numerics, 2):
+        p.append(P(node, add_numerics, (scope, c0, c1)))
+
+    return p
+
+
+def remove_zero(root, args):
+    """
+    0 + a  ->  a
     """
-    n0, n1, c0, c1 = args
+    scope, n = args
+    scope.remove(n)
 
-    if c0.is_op(OP_NEG):
-        c0 = -c0[0].value
-    else:
-        c0 = c0.value
+    return scope.as_nary_node()
 
-    if c1.is_op(OP_NEG):
-        c1 = (-c1[0].value)
-    else:
-        c1 = c1.value
 
-    scope = Scope(root)
+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()
 
     # Replace the left node with the new expression
-    scope.remove(n0, Leaf(c0 + c1))
+    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):
@@ -59,23 +87,41 @@ def match_divide_numerics(node):
 
     Example:
     6 / 2      ->  3
-    3 / 2      ->  3 / 2  # 1.5 would mean a decrease in precision
+    3 / 2      ->  3 / 2      # 1.5 would mean a decrease in precision
     3.0 / 2    ->  1.5
     3 / 2.0    ->  1.5
     3.0 / 2.0  ->  1.5
-    3 / 1.0    ->  3      # Exceptional case: division of integer by 1.0 keeps
-                          # integer precision
+    3 / 1.0    ->  3          # Exceptional case: division of integer by 1.0
+                              # keeps integer precision
+    2 / 4      ->  1 / 2      # 1 < greatest common divisor <= nominator
+    4 / 3      ->  1 + 1 / 3  # nominator > denominator
     """
     assert node.is_op(OP_DIV)
 
     n, d = node
     divide = False
-    dv = d.value
+    nv, dv = n.value, d.value
 
     if n.is_int() and d.is_int():
-        # 6 / 2  ->  3
-        # 3 / 2  ->  3 / 2
-        divide = not divmod(n.value, dv)[1]
+        mod = nv % dv
+
+        if not mod:
+            # 6 / 2  ->  3
+            # 3 / 2  ->  3 / 2
+            return [P(node, divide_numerics, (nv, dv, n.negated + d.negated))]
+
+        gcd = greatest_common_divisor(nv, dv)
+
+        if 1 < gcd <= nv:
+            # 2 / 4  ->  1 / 2
+            # TODO: Test with negations!
+            return [P(node, reduce_fraction_constants, (gcd,))]
+
+        if nv > dv:
+            # 4 / 3  ->  1 + 1 / 3
+            # TODO: Test with negations!
+            return [P(node, fraction_to_int_fraction,
+                      ((nv - mod) / dv, mod, dv))]
     elif n.is_numeric() and d.is_numeric():
         if d == 1.0:
             # 3 / 1.0  ->  3
@@ -84,14 +130,14 @@ def match_divide_numerics(node):
         # 3.0 / 2  ->  1.5
         # 3 / 2.0  ->  1.5
         # 3.0 / 2.0  ->  1.5
-        divide = True
+        return [P(node, divide_numerics, (nv, dv, n.negated + d.negated))]
 
-    return [P(node, divide_numerics, (n.value, dv))] if divide else []
+    return []
 
 
 def divide_numerics(root, args):
     """
-    Combine two constants to a single constant in a division.
+    Combine two divided constants into a single constant.
 
     Examples:
     6 / 2      ->  3
@@ -100,14 +146,48 @@ def divide_numerics(root, args):
     3.0 / 2.0  ->  1.5
     3 / 1.0    ->  3
     """
-    n, d = args
+    n, d, negated = args
 
-    return Leaf(n / d)
+    return Leaf(n / d).negate(negated)
 
 
 MESSAGES[divide_numerics] = _('Divide constant {1} by constant {2}.')
 
 
+def reduce_fraction_constants(root, args):
+    """
+    Reduce the nominator and denominator of a fraction with a given greatest
+    common divisor.
+
+    Example:
+    2 / 4  ->  1 / 2
+    """
+    gcd = args[0]
+    a, b = root
+
+    return Leaf(a.value / gcd).negate(a.negated) \
+           / Leaf(b.value / gcd).negate(b.negated)
+
+
+MESSAGES[reduce_fraction_constants] = _('Simplify fraction {0}.')
+
+
+def fraction_to_int_fraction(root, args):
+    """
+    Combine two divided integer into an integer with a fraction.
+
+    Examples:
+    4 / 3  ->  1 + 1 / 3
+    """
+    integer, nominator, denominator = map(Leaf, args)
+
+    return integer + nominator / denominator
+
+
+MESSAGES[fraction_to_int_fraction] = _('Expand fraction with nominator greater'
+    ' than denominator {0} to an integer plus a fraction.')
+
+
 def match_multiply_zero(node):
     """
     a * 0    ->  0
@@ -119,20 +199,12 @@ def match_multiply_zero(node):
     assert node.is_op(OP_MUL)
 
     left, right = node
-    is_zero = lambda n: n.is_leaf and n.value == 0
 
-    if is_zero(left):
-        negated = right.is_op(OP_NEG)
-    elif is_zero(right):
-        negated = left.is_op(OP_NEG)
-    elif left.is_op(OP_NEG) and is_zero(left[0]):
-        negated = not right.is_op(OP_NEG)
-    elif right.is_op(OP_NEG) and is_zero(right[0]):
-        negated = not left.is_op(OP_NEG)
-    else:
-        return []
+    if (left.is_leaf and left.value == 0) \
+            or (right.is_leaf and right.value == 0):
+        return [P(node, multiply_zero, (left.negated + right.negated,))]
 
-    return [P(node, multiply_zero, (negated,))]
+    return []
 
 
 def multiply_zero(root, args):
@@ -143,17 +215,48 @@ def multiply_zero(root, args):
     0 * -a   ->  -0
     -0 * -a  ->  0
     """
-    negated = args[0]
-
-    if negated:
-        return -Leaf(0)
-    else:
-        return Leaf(0)
+    return negate(Leaf(0), args[0])
 
 
 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
@@ -164,16 +267,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.value))
-        elif n.is_op(OP_NEG) and n[0].is_numeric():
-            numerics.append((n, -n[0].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
 
@@ -185,24 +283,46 @@ 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 = Scope(root)
+    scope, c0, c1 = args
 
     # 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}.')
+
+
+def match_raise_numerics(node):
+    """
+    2 ^ 3     ->  8
+    (-2) ^ 3  ->  -8
+    (-2) ^ 2  ->  4
+    """
+    assert node.is_op(OP_POW)
+
+    r, e = node
+
+    if r.is_numeric() and e.is_numeric() and not e.negated:
+        return [P(node, raise_numerics, (r, e))]
+
+    return []
+
+
+def raise_numerics(root, args):
+    """
+    2 ^ 3     ->  8
+    (-2) ^ 3  ->  -8
+    (-2) ^ 2  ->  4
+    """
+    r, e = args
+
+    return Leaf(r.value ** e.value).negate(r.negated * e.value)
+
+
+MESSAGES[raise_numerics] = _('Raise constant {1} with {2}.')

+ 0 - 96
src/rules/poly.py

@@ -1,96 +0,0 @@
-from itertools import combinations
-
-from ..node import Scope, OP_ADD, OP_NEG
-from ..possibilities import Possibility as P, MESSAGES
-from .numerics import add_numerics
-
-
-def is_numeric_or_negated_numeric(n):
-    return n.is_numeric() or (n.is_op(OP_NEG) and n[0].is_numeric())
-
-
-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(is_numeric_or_negated_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()

+ 128 - 61
src/rules/powers.py

@@ -1,7 +1,7 @@
 from itertools import combinations
 
 from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, \
-                   OP_NEG, OP_MUL, OP_DIV, OP_POW, OP_ADD
+                   OP_MUL, OP_DIV, OP_POW, OP_ADD, negate
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 
@@ -12,21 +12,23 @@ def match_add_exponents(node):
     a * a^q    ->  a^(1 + q)
     a^p * a    ->  a^(p + 1)
     a * a      ->  a^(1 + 1)
+    -a * a^q   ->  -a^(1 + q)
     """
     assert node.is_op(OP_MUL)
 
     p = []
     powers = {}
+    scope = Scope(node)
 
-    for n in Scope(node):
+    for n in scope:
+        # Order powers by their roots, e.g. a^p and a^q are put in the same
+        # list because of the mutual 'a'
         if n.is_identifier():
-            s = n
+            s = negate(n, 0)
             exponent = L(1)
         elif n.is_op(OP_POW):
-            # 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 +43,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 +52,12 @@ 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
+
+    # TODO: combine exponent negations
 
     # Replace the left node with the new expression
-    scope.remove(n0, a ** (p + q))
+    scope.replace(n0, (a ** (p + q)).negate(n0.negated + n1.negated))
 
     # Remove the right node
     scope.remove(n1)
@@ -62,7 +65,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):
@@ -91,6 +94,18 @@ def match_subtract_exponents(node):
     return []
 
 
+def subtract_exponents(root, args):
+    """
+    a^p / a^q  ->  a^(p - q)
+    """
+    a, p, q = args
+
+    return a ** (p - q)
+
+
+MESSAGES[subtract_exponents] = _('Substract the exponents {2} and {3}.')
+
+
 def match_multiply_exponents(node):
     """
     (a^p)^q  ->  a^(pq)
@@ -105,34 +120,102 @@ def match_multiply_exponents(node):
     return []
 
 
+def multiply_exponents(root, args):
+    """
+    (a^p)^q  ->  a^(pq)
+    """
+    a, p, q = args
+
+    return a ** (p * q)
+
+
+MESSAGES[multiply_exponents] = _('Multiply the exponents {2} and {3}.')
+
+
 def match_duplicate_exponent(node):
     """
     (ab)^p  ->  a^p * b^p
     """
     assert node.is_op(OP_POW)
 
-    left, right = node
+    root, exponent = node
+
+    if root.is_op(OP_MUL):
+        return [P(node, duplicate_exponent, (list(Scope(root)), exponent))]
+
+    return []
+
+
+def duplicate_exponent(root, args):
+    """
+    (ab)^p   ->  a^p * b^p
+    (abc)^p  ->  a^p * b^p * c^p
+    """
+    ab, p = args
+    result = ab[0] ** p
+
+    for b in ab[1:]:
+        result *= b ** p
+
+    return result
+
+
+MESSAGES[duplicate_exponent] = _('Duplicate the exponent {2}.')
+
 
-    if left.is_op(OP_MUL):
-        return [P(node, duplicate_exponent, (list(Scope(left)), right))]
+def match_raised_fraction(node):
+    """
+    (a / b) ^ p  ->  a^p / b^p
+    """
+    assert node.is_op(OP_POW)
+
+    root, exponent = node
+
+    if root.is_op(OP_DIV):
+        return [P(node, raised_fraction, (root, exponent))]
 
     return []
 
 
+def raised_fraction(root, args):
+    """
+    (a / b) ^ p  ->  a^p / b^p
+    """
+    (a, b), p = args
+
+    return a ** p / b ** p
+
+
+MESSAGES[raised_fraction] = _('Apply the exponent {2} to the nominator and'
+        ' denominator of fraction {1}.')
+
+
 def match_remove_negative_exponent(node):
     """
-    a^-p  ->  1 / a^p
+    a ^ -p  ->  1 / a ^ p
     """
     assert node.is_op(OP_POW)
 
-    left, right = node
+    a, p = node
 
-    if right.is_op(OP_NEG):
-        return [P(node, remove_negative_exponent, (left, right[0]))]
+    if p.negated:
+        return [P(node, remove_negative_exponent, (a, p))]
 
     return []
 
 
+def remove_negative_exponent(root, args):
+    """
+    a^-p  ->  1 / a^p
+    """
+    a, p = args
+
+    return L(1) / a ** p.reduce_negation()
+
+
+MESSAGES[remove_negative_exponent] = _('Remove negative exponent {2}.')
+
+
 def match_exponent_to_root(node):
     """
     a^(1 / m)  ->  sqrt(a, m)
@@ -148,6 +231,16 @@ def match_exponent_to_root(node):
     return []
 
 
+def exponent_to_root(root, args):
+    """
+    a^(1 / m)  ->  sqrt(a, m)
+    a^(n / m)  ->  sqrt(a^n, m)
+    """
+    a, n, m = args
+
+    return N('sqrt', a if n == 1 else a ** n, m)
+
+
 def match_extend_exponent(node):
     """
     (a + ... + z)^n -> (a + ... + z)(a + ... + z)^(n - 1)  # n > 1
@@ -176,64 +269,38 @@ def extend_exponent(root, args):
     return left * left
 
 
-def subtract_exponents(root, args):
+def match_constant_exponent(node):
     """
-    a^p / a^q  ->  a^(p - q)
+    (a + ... + z)^n -> (a + ... + z)(a + ... + z)^(n - 1)  # n > 1
     """
-    a, p, q = args
-
-    return a ** (p - q)
-
-
-MESSAGES[subtract_exponents] = _('Substract the exponents {2} and {3}.')
-
+    assert node.is_op(OP_POW)
 
-def multiply_exponents(root, args):
-    """
-    (a^p)^q  ->  a^(pq)
-    """
-    a, p, q = args
+    exponent = node[1]
 
-    return a ** (p * q)
+    if exponent == 0:
+        return [P(node, remove_power_of_zero, ())]
 
+    if exponent == 1:
+        return [P(node, remove_power_of_one, ())]
 
-MESSAGES[multiply_exponents] = _('Multiply the exponents {2} and {3}.')
+    return []
 
 
-def duplicate_exponent(root, args):
+def remove_power_of_zero(root, args):
     """
-    (ab)^p   ->  a^p * b^p
-    (abc)^p  ->  a^p * b^p * c^p
+    a ^ 0  ->  1
     """
-    ab, p = args
-    result = ab[0] ** p
-
-    for b in ab[1:]:
-        result *= b ** p
-
-    return result
+    return L(1)
 
 
-MESSAGES[duplicate_exponent] = _('Duplicate the exponent {2}.')
+MESSAGES[remove_power_of_zero] = _('Power of zero {0} rewrites to 1.')
 
 
-def remove_negative_exponent(root, args):
+def remove_power_of_one(root, args):
     """
-    a^-p  ->  1 / a^p
+    a ^ 1  ->  a
     """
-    a, p = args
-
-    return L(1) / a ** p
-
-
-MESSAGES[remove_negative_exponent] = _('Remove negative exponent {2}.')
+    return root[0]
 
 
-def exponent_to_root(root, args):
-    """
-    a^(1 / m)  ->  sqrt(a, m)
-    a^(n / m)  ->  sqrt(a^n, m)
-    """
-    a, n, m = args
-
-    return N('sqrt', a if n == 1 else a ** n, m)
+MESSAGES[remove_power_of_one] = _('Remove the power of one in {0}.')

+ 37 - 0
src/rules/sort.py

@@ -0,0 +1,37 @@
+from itertools import product, combinations
+
+from ..node import Scope, OP_ADD, OP_MUL
+from ..possibilities import Possibility as P, MESSAGES
+from ..translate import _
+
+
+def match_sort_multiplicants(node):
+    """
+    Sort multiplicant factors by swapping
+    x * 2  ->  2x
+    """
+    assert node.is_op(OP_MUL)
+
+    p = []
+    scope = Scope(node)
+
+    for i, n in enumerate(scope[1:]):
+        left_nb = scope[i]
+
+        if n.is_numeric() and not left_nb.is_numeric():
+            p.append(P(node, move_constant, (scope, n, left_nb)))
+
+    return p
+
+
+def move_constant(root, args):
+    scope, constant, destination = args
+
+    scope.replace(destination, constant * destination)
+    scope.remove(constant)
+
+    return scope.as_nary_node()
+
+
+MESSAGES[move_constant] = \
+        _('Move constant {2} to the left of the multiplication {0}.')

+ 54 - 3
src/rules/utils.py

@@ -1,6 +1,9 @@
-def gcd(a, b):
+from ..node import ExpressionLeaf as L, OP_MUL, OP_DIV
+
+
+def greatest_common_divisor(a, b):
     """
-    Return greatest common divisor using Euclid's Algorithm.
+    Return greatest common divisor of a and b using Euclid's Algorithm.
     """
     while b:
         a, b = b, a % b
@@ -12,7 +15,7 @@ def lcm(a, b):
     """
     Return least common multiple of a and b.
     """
-    return a * b // gcd(a, b)
+    return a * b // greatest_common_divisor(a, b)
 
 
 def least_common_multiple(*args):
@@ -20,3 +23,51 @@ def least_common_multiple(*args):
     Return lcm of args.
     """
     return reduce(lcm, args)
+
+
+def is_fraction(node, nominator, denominator):
+    """
+    Check if a node represents the fraction of a given nominator and
+    denominator.
+
+    >>> from ..node import ExpressionLeaf as L
+    >>> l1, l2, a = L('a'), L(1), L(2)
+    >>> is_fraction(a / l2, a, 2)
+    True
+    >>> is_fraction(l1 / l2 * a, a, 2)
+    True
+    >>> is_fraction(l2 / l1 * a, a, 2)
+    False
+    """
+    if node.is_op(OP_DIV):
+        nom, denom = node
+
+        return nom == nominator and denom == denominator
+
+    if node.is_op(OP_MUL):
+        # 1 / denominator * nominator
+        # nominator * 1 / denominator
+        left, right = node
+        fraction = L(1) / denominator
+
+        return (left == nominator and right == fraction) \
+               or (right == nominator and left == fraction)
+
+    return False
+
+
+def partition(callback, iterable):
+    """
+    Partition an iterable into two parts using a callback that returns a
+    boolean.
+
+    Example:
+    >>> partition(lambda x: x & 1, range(6))
+    ([1, 3, 5], [0, 2, 4])
+    """
+    a, b = [], []
+
+    for item in iterable:
+        (a if callback(item) else b).append(item)
+
+    return a, b

+ 18 - 0
src/strategy.py

@@ -0,0 +1,18 @@
+from rules.sort import move_constant
+from rules.numerics import reduce_fraction_constants, fraction_to_int_fraction
+
+
+def pick_suggestion(possibilities):
+    if not possibilities:
+        return
+
+    # TODO: pick the best suggestion.
+    for suggestion, p in enumerate(possibilities + [None]):
+        if p and p.handler not in [move_constant, fraction_to_int_fraction,
+                reduce_fraction_constants]:
+            break
+
+    if not p:
+        return possibilities[0]
+
+    return possibilities[suggestion]

+ 22 - 0
src/unicode_math.py

@@ -0,0 +1,22 @@
+# vim: set fileencoding=utf-8 :
+SQRT = '√'
+CBRT = '∛'
+FORT = '∜'
+
+PI = 'π'
+INFINITY = '∞'
+
+SUP = {
+    '0': '⁰',
+    '1': '¹',
+    '2': '²',
+    '3': '³',
+    '3': '⁴',
+    '5': '⁵',
+    '6': '⁶',
+    '7': '⁷',
+    '8': '⁸',
+    '9': '⁹',
+}
+
+DOT = '⋅'

+ 32 - 0
src/validation.py

@@ -0,0 +1,32 @@
+from src.parser import Parser
+from tests.parser import ParserWrapper
+from src.possibilities import apply_suggestion
+
+
+def validate(exp, result):
+    """
+    Validate that exp =>* result.
+    """
+    parser = ParserWrapper(Parser)
+    result = parser.run([result])
+
+    return traverse_preorder(parser, exp, result)
+
+
+def traverse_preorder(parser, exp, result):
+    """
+    Traverse the possibility tree using pre-order traversal.
+    """
+    root = parser.run([exp])
+
+    if root.equals(result):
+        return root
+
+    possibilities = parser.parser.possibilities
+
+    for p in possibilities:
+        child = apply_suggestion(root, p)
+        next_root = traverse_preorder(parser, str(child), result)
+
+        if next_root:
+            return next_root

+ 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'])

+ 110 - 77
tests/test_leiden_oefenopgave.py

@@ -4,10 +4,10 @@ from tests.rulestestcase import RulesTestCase as TestCase, rewrite
 class TestLeidenOefenopgave(TestCase):
     def test_1_1(self):
         for chain in [['-5(x2 - 3x + 6)', '-5(x ^ 2 - 3x) - 5 * 6',
-                       '-5 * x ^ 2 - 5 * -3x - 5 * 6',
-                       '-5 * x ^ 2 - -15x - 5 * 6',
-                       '-5 * x ^ 2 + 15x - 5 * 6',
-                       '-5 * x ^ 2 + 15x - 30',
+                       '-5x ^ 2 - 5 * -3x - 5 * 6',
+                       '-5x ^ 2 - -15x - 5 * 6',
+                       '-5x ^ 2 + 15x - 5 * 6',
+                       '-5x ^ 2 + 15x - 30',
                        ],
                      ]:
             self.assertRewrite(chain)
@@ -15,80 +15,103 @@ class TestLeidenOefenopgave(TestCase):
         return
 
         for exp, solution in [
-                ('-5(x2 - 3x + 6)',       '-30 + 15 * x - 5 * x ^ 2'),
-                ('(x+1)^2',              'x ^ 2 + 2 * x + 1'),
-                ('(x-1)^2',              'x ^ 2 - 2 * x + 1'),
-                ('(2x+x)*x',             '3 * x ^ 2'),
-                ('-2(6x-4)^2*x',         '-72 * x^3 + 96 * x ^ 2 + 32 * x'),
+                ('-5(x2 - 3x + 6)',       '-30 + 15x - 5x ^ 2'),
+                ('(x+1)^2',              'x ^ 2 + 2x + 1'),
+                ('(x-1)^2',              'x ^ 2 - 2x + 1'),
+                ('(2x+x)*x',             '3x ^ 2'),
+                ('-2(6x-4)^2*x',         '-72x ^ 3 + 96x ^ 2 + 32x'),
                 ('(4x + 5) * -(5 - 4x)', '16x^2 - 25'),
                 ]:
             self.assertEqual(str(rewrite(exp)), solution)
 
     def test_1_2(self):
-        for chain in [['(x+1)^3', '(x + 1)(x + 1) ^ 2',
-                '(x + 1)(x + 1)(x + 1)',
-                '(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 + (1 + 1)x + 1 * 1)(x + 1)',
-                '(x ^ 2 + 2x + 1 * 1)(x + 1)',
-                '(x ^ 2 + 2x + 1)(x + 1)',
-                '(x ^ 2 + 2x)x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
-                'x * x ^ 2 + x * 2x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
-                'x ^ (1 + 2) + x * 2x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
-                'x ^ 3 + x * 2x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
-                '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 + (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 + (2 + 1)x + 1 * 1',
-                'x ^ 3 + 3 * x ^ 2 + 3x + 1 * 1',
-                'x ^ 3 + 3 * x ^ 2 + 3x + 1',
-                ]
-            ]:
-            self.assertRewrite(chain)
+        self.assertRewrite(['(x+1)^3', '(x + 1)(x + 1) ^ 2',
+                            '(x + 1)(x + 1)(x + 1)',
+                            '(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)',
+                            '(x ^ 2 + 2x)x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
+                            'xx ^ 2 + x * 2x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
+                            'x ^ (1 + 2) + x * 2x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
+                            'x ^ 3 + x * 2x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
+                            '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 + 1x ^ 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 + 3x ^ 2 + 1 * 2x + 1x + 1 * 1',
+                            'x ^ 3 + 3x ^ 2 + 2x + 1x + 1 * 1',
+                            'x ^ 3 + 3x ^ 2 + 2x + x + 1 * 1',
+                            'x ^ 3 + 3x ^ 2 + (2 + 1)x + 1 * 1',
+                            'x ^ 3 + 3x ^ 2 + 3x + 1 * 1',
+                            'x ^ 3 + 3x ^ 2 + 3x + 1',
+                            ])
 
     def test_1_3(self):
         # (x+1)^2 -> x^2 + 2x + 1
-        for chain in [['(x+1)^2', '(x + 1)(x + 1)',
+        self.assertRewrite(['(x+1)^2', '(x + 1)(x + 1)',
                        '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'],
-                     ]:
-            self.assertRewrite(chain)
+                       'x ^ 2 + 2x + 1',
+                       ])
 
     def test_1_4(self):
         # (x-1)^2 -> x^2 - 2x + 1
-        for chain in [['(x-1)^2', '(x - 1)(x - 1)',
-                       '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',
-                     ]]:
-            self.assertRewrite(chain)
+        self.assertRewrite(['(x-1)^2', '(x - 1)(x - 1)',
+                            'xx + x * -1 - 1x - 1 * -1',
+                            'x ^ (1 + 1) + x * -1 - 1x - 1 * -1',
+                            'x ^ 2 + 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 + 2 * -x - 1 * -1',
+                            'x ^ 2 - 2x - 1 * -1',
+                            'x ^ 2 - 2x - -1',
+                            'x ^ 2 - 2x + 1',
+                            ])
 
     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',
-                            '3 * x ^ (1 + 1)', '3 * x ^ 2'])
+                            '3x ^ (1 + 1)', '3x ^ 2'])
 
     def test_1_7(self):
         self.assertRewrite(['(4x + 5) * -(5 - 4x)',
@@ -97,15 +120,14 @@ class TestLeidenOefenopgave(TestCase):
                             '4x * -5 + 4x * 4x + 5 * -5 + 5 * 4x',
                             '-20x + 4x * 4x + 5 * -5 + 5 * 4x',
                             '-20x + 16xx + 5 * -5 + 5 * 4x',
-                            '-20x + 16 * x ^ (1 + 1) + 5 * -5 + 5 * 4x',
-                            '-20x + 16 * x ^ 2 + 5 * -5 + 5 * 4x',
-                            '-20x + 16 * x ^ 2 - 25 + 5 * 4x',
-                            '-20x + 16 * x ^ 2 - 25 + 20x',
-                            '(-20 + 20)x + 16 * x ^ 2 - 25',
-                            '0x + 16 * x ^ 2 - 25',
-                            '0 + 16 * x ^ 2 - 25',
-                            '-25 + 16 * x ^ 2'])
-                            # FIXME: '16 * x ^ 2 - 25'])
+                            '-20x + 16x ^ (1 + 1) + 5 * -5 + 5 * 4x',
+                            '-20x + 16x ^ 2 + 5 * -5 + 5 * 4x',
+                            '-20x + 16x ^ 2 - 25 + 5 * 4x',
+                            '-20x + 16x ^ 2 - 25 + 20x',
+                            '(-20 + 20)x + 16x ^ 2 - 25',
+                            '0x + 16x ^ 2 - 25',
+                            '0 + 16x ^ 2 - 25',
+                            '16x ^ 2 - 25'])
 
     def test_2(self):
         pass
@@ -113,18 +135,29 @@ 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 * 3 / (3 * 5)',
+                            '21 / (3 * 5)',
+                            '21 / 15',
+                            '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 / 4 / 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))'])

+ 114 - 0
tests/test_leiden_oefenopgave_v12.py

@@ -0,0 +1,114 @@
+from tests.rulestestcase import RulesTestCase as TestCase
+
+
+class TestLeidenOefenopgaveV12(TestCase):
+    def test_1_a(self):
+        self.assertRewrite(['-5(x2 - 3x + 6)',
+                            '-5(x ^ 2 - 3x) - 5 * 6',
+                            '-5x ^ 2 - 5 * -3x - 5 * 6',
+                            '-5x ^ 2 - -15x - 5 * 6',
+                            '-5x ^ 2 + 15x - 5 * 6',
+                            '-5x ^ 2 + 15x - 30'])
+
+    def test_1_d(self):
+        self.assertRewrite(['(2x + x)x',
+                            '(2 + 1)xx',
+                            '3xx',
+                            '3x ^ (1 + 1)',
+                            '3x ^ 2'])
+
+    def test_1_e(self):
+        self.assertRewrite([
+            '-2(6x - 4) ^ 2x',
+            '-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',
+            '(-72x ^ (1 + 1) - 12x * -4 + 8 * 6x + 8 * -4)x',
+            '(-72x ^ 2 - 12x * -4 + 8 * 6x + 8 * -4)x',
+            '(-72x ^ 2 - -48x + 8 * 6x + 8 * -4)x',
+            '(-72x ^ 2 + 48x + 8 * 6x + 8 * -4)x',
+            '(-72x ^ 2 + 48x + 48x + 8 * -4)x',
+            '(-72x ^ 2 + (1 + 1) * 48x + 8 * -4)x',
+            '(-72x ^ 2 + 2 * 48x + 8 * -4)x',
+            '(-72x ^ 2 + 96x + 8 * -4)x',
+            '(-72x ^ 2 + 96x - 32)x',
+            'x(-72x ^ 2 + 96x) + x * -32',
+            'x * -72x ^ 2 + x * 96x + x * -32',
+            '-x * 72x ^ 2 + x * 96x + x * -32',
+            '-x ^ (1 + 2) * 72 + x * 96x + x * -32',
+            '-x ^ 3 * 72 + x * 96x + x * -32',
+            '-x ^ 3 * 72 + x ^ (1 + 1) * 96 + x * -32',
+            '-x ^ 3 * 72 + x ^ 2 * 96 + x * -32',
+            '-x ^ 3 * 72 + x ^ 2 * 96 - x * 32',
+            '72 * -x ^ 3 + x ^ 2 * 96 - x * 32',
+            '-72x ^ 3 + x ^ 2 * 96 - x * 32',
+            '-72x ^ 3 + 96x ^ 2 - x * 32',
+            '-72x ^ 3 + 96x ^ 2 + 32 * -x',
+            '-72x ^ 3 + 96x ^ 2 - 32x',
+        ])
+
+    def test_2_a(self):
+        self.assertRewrite([
+            '(a2b^-1)^3(ab2)',
+            '(a ^ 2 * (1 / b ^ 1)) ^ 3 * ab ^ 2',
+            '(a ^ 2 * (1 / b)) ^ 3 * ab ^ 2',
+            '(a ^ 2 * 1 / b) ^ 3 * ab ^ 2',
+            '(a ^ 2 / b) ^ 3 * ab ^ 2',
+            '(a ^ 2) ^ 3 / b ^ 3 * ab ^ 2',
+            'a ^ (2 * 3) / b ^ 3 * ab ^ 2',
+            'a ^ 6 / b ^ 3 * ab ^ 2',
+            'aa ^ 6 / b ^ 3 * b ^ 2',
+            'a ^ (1 + 6) / b ^ 3 * b ^ 2',
+            'a ^ 7 / b ^ 3 * b ^ 2',
+            'b ^ 2 * a ^ 7 / b ^ 3',
+            'b ^ 2 / b ^ 3 * a ^ 7 / 1',
+            'b ^ (2 - 3)a ^ 7 / 1',
+            'b ^ -1 * a ^ 7 / 1',
+            '1 / b ^ 1 * a ^ 7 / 1',
+            '1 / b * a ^ 7 / 1',
+            'a ^ 7 * 1 / b / 1',
+            'a ^ 7 / b / 1',
+            'a ^ 7 / b',
+        ])
+
+    def test_2_b(self):
+        self.assertRewrite([
+            'a3b2a3',
+            'a ^ (3 + 3)b ^ 2',
+            'a ^ 6 * b ^ 2',
+        ])
+
+    def test_2_c(self):
+        self.assertRewrite([
+            'a5+a3',
+            'a ^ 5 + a ^ 3',
+        ])
+
+    def test_2_d(self):
+        self.assertRewrite([
+            'a2+a2',
+            '(1 + 1)a ^ 2',
+            '2a ^ 2',
+        ])
+
+    def test_2_e(self):
+        self.assertRewrite([
+            '4b^-2',
+            '4(1 / b ^ 2)',
+            '4 * 1 / b ^ 2',
+        ])
+
+    def test_2_f(self):
+        self.assertRewrite([
+            '(4b) ^ -2',
+            '4 ^ -2 * b ^ -2',
+            '1 / 4 ^ 2 * b ^ -2',
+            '1 / 16 * b ^ -2',
+            '1 / 16 * (1 / b ^ 2)',
+            '1 * 1 / (16b ^ 2)',
+            '1 / (16b ^ 2)',
+        ])

+ 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)

+ 35 - 2
tests/test_parser.py

@@ -4,6 +4,8 @@ import unittest
 from src.parser import Parser
 from src.node import ExpressionNode as Node, ExpressionLeaf as Leaf
 from tests.parser import ParserWrapper, run_expressions, line, graph
+from tests.rulestestcase import tree
+from src.rules.goniometry import sin, cos
 
 
 class TestParser(unittest.TestCase):
@@ -15,11 +17,42 @@ class TestParser(unittest.TestCase):
         run_expressions(Parser, [('a', Leaf('a'))])
 
     def test_graph(self):
-        assert graph(Parser, '4a') == ("""
+        self.assertEqual(graph(Parser, '4a'), ("""
          *
         ╭┴╮
         4 a
-        """).replace('\n        ', '\n')[1:-1]
+        """).replace('\n        ', '\n')[1:-1])
 
     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)
+
+    def test_moved_negation(self):
+        a, b = tree('a,b')
+
+        self.assertEqual(tree('-ab'), (-a) * b)
+        self.assertEqual(tree('-(ab)'), (-a) * b)
+        self.assertEqual(tree('-a / b'), (-a) / b)
+        self.assertEqual(tree('-(a / b)'), (-a) / b)
+
+    def test_functions(self):
+        root, x = tree('sin x, x')
+
+        self.assertEqual(root, sin(x))
+        self.assertEqual(tree('sin x ^ 2'), sin(x) ** 2)
+        self.assertEqual(tree('sin(x) ^ 2'), sin(x) ** 2)
+        self.assertEqual(tree('sin (x) ^ 2'), sin(x) ** 2)
+        self.assertEqual(tree('sin(x ^ 2)'), sin(x ** 2))
+        self.assertEqual(tree('sin cos x'), sin(cos(x)))
+        self.assertEqual(tree('sin cos x ^ 2'), sin(cos(x)) ** 2)

+ 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')

+ 135 - 12
tests/test_rules_fractions.py

@@ -1,6 +1,10 @@
 from src.rules.fractions import match_constant_division, division_by_one, \
         division_of_zero, division_by_self, match_add_constant_fractions, \
-        equalize_denominators, add_nominators
+        equalize_denominators, add_nominators, match_multiply_fractions, \
+        multiply_fractions, multiply_with_fraction, match_divide_fractions, \
+        divide_fraction, divide_by_fraction, match_equal_fraction_parts, \
+        divide_fraction_parts, extract_divided_roots
+from src.node import Scope
 from src.possibilities import Possibility as P
 from tests.rulestestcase import RulesTestCase, tree
 
@@ -51,12 +55,14 @@ class TestRulesFractions(RulesTestCase):
         n0, n1 = root = l1 / l2 + l3 / l4
         possibilities = match_add_constant_fractions(root)
         self.assertEqualPos(possibilities,
-                [P(root, equalize_denominators, (n0, n1, 4))])
+                [P(root, equalize_denominators, (Scope(root), n0, n1, 4)),
+                 P(root, equalize_denominators, (Scope(root), n0, n1, 8))])
 
         (((n0, n1), n2), n3), n4 = root = a + l1 / l2 + b + l3 / l4 + c
         possibilities = match_add_constant_fractions(root)
         self.assertEqualPos(possibilities,
-                [P(root, equalize_denominators, (n1, n3, 4))])
+                [P(root, equalize_denominators, (Scope(root), n1, n3, 4)),
+                 P(root, equalize_denominators, (Scope(root), n1, n3, 8))])
 
         n0, n1 = root = l2 / l4 + l3 / l4
         possibilities = match_add_constant_fractions(root)
@@ -74,7 +80,8 @@ class TestRulesFractions(RulesTestCase):
         (((n0, n1), n2), n3), n4 = root = a + l2 / l2 + b + (-l3 / l4) + c
         possibilities = match_add_constant_fractions(root)
         self.assertEqualPos(possibilities,
-                [P(root, equalize_denominators, (n1, n3, 4))])
+                [P(root, equalize_denominators, (Scope(root), n1, n3, 4)),
+                 P(root, equalize_denominators, (Scope(root), n1, n3, 8))])
 
         (((n0, n1), n2), n3), n4 = root = a + l2 / l4 + b + (-l3 / l4) + c
         possibilities = match_add_constant_fractions(root)
@@ -85,22 +92,23 @@ class TestRulesFractions(RulesTestCase):
         a, b, l1, l2, l3, l4 = tree('a,b,1,2,3,4')
 
         n0, n1 = root = l1 / l2 + l3 / l4
-        self.assertEqualNodes(equalize_denominators(root, (n0, n1, 4)),
-                              l2 / l4 + l3 / l4)
+        self.assertEqualNodes(equalize_denominators(root,
+                              (Scope(root), n0, n1, 4)), l2 / l4 + l3 / l4)
 
         n0, n1 = root = a / l2 + b / l4
-        self.assertEqualNodes(equalize_denominators(root, (n0, n1, 4)),
-                              (l2 * a) / l4 + b / l4)
+        self.assertEqualNodes(equalize_denominators(root,
+                              (Scope(root), n0, n1, 4)), (l2 * a) / l4 + b /
+                              l4)
 
         #2 / 2 - 3 / 4  ->  4 / 4 - 3 / 4  # Equalize denominators
         n0, n1 = root = l1 / l2 + (-l3 / l4)
-        self.assertEqualNodes(equalize_denominators(root, (n0, n1, 4)),
-                              l2 / l4 + (-l3 / l4))
+        self.assertEqualNodes(equalize_denominators(root,
+            (Scope(root), n0, n1, 4)), l2 / l4 + (-l3 / l4))
 
         #2 / 2 - 3 / 4  ->  4 / 4 - 3 / 4  # Equalize denominators
         n0, n1 = root = a / l2 + (-b / l4)
-        self.assertEqualNodes(equalize_denominators(root, (n0, n1, 4)),
-                              (l2 * a) / l4 + (-b / l4))
+        self.assertEqualNodes(equalize_denominators(root,
+            (Scope(root), n0, n1, 4)), (l2 * a) / l4 + (-b / l4))
 
     def test_add_nominators(self):
         a, b, c = tree('a,b,c')
@@ -118,3 +126,118 @@ class TestRulesFractions(RulesTestCase):
 
         n0, n1 = root = a / -b + -c / -b
         self.assertEqualNodes(add_nominators(root, (n0, n1)), (a + -c) / -b)
+
+    def test_match_multiply_fractions(self):
+        (a, b), (c, d) = ab, cd = root = tree('a / b * (c / d)')
+        self.assertEqualPos(match_multiply_fractions(root),
+                [P(root, multiply_fractions, (Scope(root), ab, cd))])
+
+        (ab, e), cd = root = tree('a / b * e * (c / d)')
+        self.assertEqualPos(match_multiply_fractions(root),
+                [P(root, multiply_fractions, (Scope(root), ab, cd)),
+                 P(root, multiply_with_fraction, (Scope(root), e, ab)),
+                 P(root, multiply_with_fraction, (Scope(root), e, cd))])
+
+    def test_multiply_fractions(self):
+        (a, b), (c, d) = ab, cd = root = tree('a / b * (c / d)')
+        self.assertEqual(multiply_fractions(root, (Scope(root), ab, cd)),
+                         a * c / (b * d))
+
+        (ab, e), cd = root = tree('a / b * e * (c / d)')
+        self.assertEqual(multiply_fractions(root, (Scope(root), ab, cd)),
+                         a * c / (b * d) * e)
+
+    def test_match_divide_fractions(self):
+        (a, b), c = root = tree('a / b / c')
+        self.assertEqualPos(match_divide_fractions(root),
+                [P(root, divide_fraction, (a, b, c))])
+
+        root = tree('a / (b / c)')
+        self.assertEqualPos(match_divide_fractions(root),
+                [P(root, divide_by_fraction, (a, b, c))])
+
+    def test_divide_fraction(self):
+        (a, b), c = root = tree('a / b / c')
+        self.assertEqual(divide_fraction(root, (a, b, c)), a / (b * c))
+
+    def test_divide_by_fraction(self):
+        a, (b, c) = root = tree('a / (b / c)')
+        self.assertEqual(divide_by_fraction(root, (a, b, c)), a * c / b)
+
+    def test_match_equal_fraction_parts(self):
+        (a, b), (c, a) = root = tree('ab / (ca)')
+        self.assertEqualPos(match_equal_fraction_parts(root),
+                [P(root, divide_fraction_parts, (a, [a, b], [c, a], 0, 1))])
+
+        (a, b), a = root = tree('ab / a')
+        self.assertEqualPos(match_equal_fraction_parts(root),
+                [P(root, divide_fraction_parts, (a, [a, b], [a], 0, 0))])
+
+        a, (a, b) = root = tree('a / (ab)')
+        self.assertEqualPos(match_equal_fraction_parts(root),
+                [P(root, divide_fraction_parts, (a, [a], [a, b], 0, 0))])
+
+        root = tree('abc / (cba)')
+        ((a, b), c) = root[0]
+        s0, s1 = [a, b, c], [c, b, a]
+        self.assertEqualPos(match_equal_fraction_parts(root),
+                [P(root, divide_fraction_parts, (a, s0, s1, 0, 2)),
+                 P(root, divide_fraction_parts, (b, s0, s1, 1, 1)),
+                 P(root, divide_fraction_parts, (c, s0, s1, 2, 0))])
+
+        root = tree('-a / a')
+        self.assertEqualPos(match_equal_fraction_parts(root),
+                [P(root, divide_fraction_parts, (a, [-a], [a], 0, 0))])
+
+        (ap, b), aq = root = tree('a ^ p * b / a ^ q')
+        self.assertEqualPos(match_equal_fraction_parts(root),
+                [P(root, extract_divided_roots, (a, [ap, b], [aq], 0, 0))])
+
+        (a, b), aq = root = tree('a * b / a ^ q')
+        self.assertEqualPos(match_equal_fraction_parts(root),
+                [P(root, extract_divided_roots, (a, [a, b], [aq], 0, 0))])
+
+        (ap, b), a = root = tree('a ^ p * b / a')
+        self.assertEqualPos(match_equal_fraction_parts(root),
+                [P(root, extract_divided_roots, (a, [ap, b], [a], 0, 0))])
+
+    def test_divide_fraction_parts(self):
+        (a, b), (c, a) = root = tree('ab / (ca)')
+        result = divide_fraction_parts(root, (a, [a, b], [c, a], 0, 1))
+        self.assertEqual(result, b / c)
+
+        (a, b), a = root = tree('ab / a')
+        result = divide_fraction_parts(root, (a, [a, b], [a], 0, 0))
+        self.assertEqual(result, b / 1)
+
+        root, l1 = tree('a / (ab), 1')
+        a, (a, b) = root
+        result = divide_fraction_parts(root, (a, [a], [a, b], 0, 0))
+        self.assertEqual(result, l1 / b)
+
+        root = tree('abc / (cba)')
+        ((a, b), c) = root[0]
+        result = divide_fraction_parts(root, (a, [a, b, c], [c, b, a], 0, 2))
+        self.assertEqual(result, b * c / (c * b))
+        result = divide_fraction_parts(root, (b, [a, b, c], [c, b, a], 1, 1))
+        self.assertEqual(result, a * c / (c * a))
+        result = divide_fraction_parts(root, (c, [a, b, c], [c, b, a], 2, 0))
+        self.assertEqual(result, a * b / (b * a))
+
+        (a, b), a = root = tree('-ab / a')
+        result = divide_fraction_parts(root, (a, [-a, b], [a], 0, 0))
+        self.assertEqual(result, -b / 1)
+
+    def test_extract_divided_roots(self):
+        r, a = tree('a ^ p * b / a ^ q, a')
+        ((a, p), b), (a, q) = (ap, b), aq = r
+        self.assertEqual(extract_divided_roots(r, (a, [ap, b], [aq], 0, 0)),
+                         a ** p / a ** q * b / 1)
+
+        r = tree('a * b / a ^ q, a')
+        self.assertEqual(extract_divided_roots(r, (a, [a, b], [aq], 0, 0)),
+                         a / a ** q * b / 1)
+
+        r = tree('a ^ p * b / a, a')
+        self.assertEqual(extract_divided_roots(r, (a, [ap, b], [a], 0, 0)),
+                         a ** p / a * b / 1)

+ 71 - 0
tests/test_rules_goniometry.py

@@ -0,0 +1,71 @@
+# vim: set fileencoding=utf-8 :
+from src.rules.goniometry import sin, cos, tan, match_add_quadrants, \
+        add_quadrants, match_negated_parameter, negated_sinus_parameter, \
+        negated_cosinus_parameter, match_standard_radian, standard_radian, \
+        is_pi_frac
+from src.node import PI, OP_SIN, OP_COS, OP_TAN
+from src.possibilities import Possibility as P
+from tests.rulestestcase import RulesTestCase, tree
+from src.rules import goniometry
+import doctest
+
+
+class TestRulesGoniometry(RulesTestCase):
+
+    def test_doctest(self):
+        self.assertEqual(doctest.testmod(m=goniometry)[0], 0)
+
+    def test_match_add_quadrants(self):
+        root = tree('sin t ^ 2 + cos t ^ 2')
+        possibilities = match_add_quadrants(root)
+        self.assertEqualPos(possibilities, [P(root, add_quadrants, ())])
+
+    def test_add_quadrants(self):
+        self.assertEqual(add_quadrants(None, ()), 1)
+
+    def test_match_negated_parameter(self):
+        s, c = tree('sin -t, cos -t')
+        t = s[0]
+
+        self.assertEqualPos(match_negated_parameter(s), \
+                [P(s, negated_sinus_parameter, (t,))])
+
+        self.assertEqualPos(match_negated_parameter(c), \
+                [P(c, negated_cosinus_parameter, (t,))])
+
+    def test_negated_sinus_parameter(self):
+        s = tree('sin -t')
+        t = s[0]
+        self.assertEqual(negated_sinus_parameter(s, (t,)), -sin(+t))
+
+    def test_negated_cosinus_parameter(self):
+        c = tree('cos -t')
+        t = c[0]
+        self.assertEqual(negated_cosinus_parameter(c, (t,)), cos(+t))
+
+    def test_is_pi_frac(self):
+        l1, pi = tree('1,' + PI)
+
+        self.assertTrue(is_pi_frac(l1 / 2 * pi, 2))
+        self.assertFalse(is_pi_frac(l1 / 2 * pi, 3))
+        self.assertFalse(is_pi_frac(l1 * pi, 3))
+
+    def test_match_standard_radian(self):
+        s, c, t = tree('sin(1 / 6 * pi), cos(1 / 2 * pi), tan(0)')
+
+        self.assertEqualPos(match_standard_radian(s), \
+                [P(s, standard_radian, (OP_SIN, 1))])
+
+        self.assertEqualPos(match_standard_radian(c), \
+                [P(c, standard_radian, (OP_COS, 4))])
+
+        self.assertEqualPos(match_standard_radian(t), \
+                [P(t, standard_radian, (OP_TAN, 0))])
+
+    def test_standard_radian(self):
+        l0, l1, sq3, pi6, pi4, pi2 = tree('0,1,sqrt(3),1/6*pi,1/4*pi,1/2*pi')
+
+        self.assertEqual(standard_radian(sin(pi6), (OP_SIN, 1)), l1 / 2)
+        self.assertEqual(standard_radian(sin(pi2), (OP_SIN, 4)), 1)
+        self.assertEqual(standard_radian(cos(l0), (OP_COS, 0)), 1)
+        self.assertEqual(standard_radian(tan(pi4), (OP_TAN, 3)), sq3)

+ 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)

+ 54 - 16
tests/test_rules_negation.py

@@ -1,44 +1,82 @@
-
-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')), [])
 
     def test_match_negated_division_single(self):
         l1, l2 = root = tree('-1 / 2')
-        possibilities = match_negated_division(root)
-        self.assertEqualPos(possibilities,
-                [P(root, single_negated_division, (l1[0], l2))])
+        self.assertEqualPos(match_negated_division(root), [])
 
         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))
-
         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)

+ 95 - 43
tests/test_rules_numerics.py

@@ -1,58 +1,84 @@
-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, reduce_fraction_constants, \
+        fraction_to_int_fraction, match_multiply_numerics, multiply_numerics, \
+        raise_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')
+        a, b, i2, i3, i4, i6, f1, f2, f3 = tree('a,b,2,3,4,6,1.0,2.0,3.0')
 
         root = i6 / i2
         possibilities = match_divide_numerics(root)
         self.assertEqualPos(possibilities,
-                [P(root, divide_numerics, (6, 2))])
+                [P(root, divide_numerics, (6, 2, 0))])
+
+        root = -i6 / i2
+        possibilities = match_divide_numerics(root)
+        self.assertEqualPos(possibilities,
+                [P(root, divide_numerics, (6, 2, 1))])
 
         root = i3 / i2
         possibilities = match_divide_numerics(root)
-        self.assertEqualPos(possibilities, [])
+        self.assertEqualPos(possibilities,
+                [P(root, fraction_to_int_fraction, (1, 1, 2))])
+
+        root = i2 / i4
+        possibilities = match_divide_numerics(root)
+        self.assertEqualPos(possibilities,
+                [P(root, reduce_fraction_constants, (2,))])
 
         root = f3 / i2
         possibilities = match_divide_numerics(root)
         self.assertEqualPos(possibilities,
-                [P(root, divide_numerics, (3.0, 2))])
+                [P(root, divide_numerics, (3.0, 2, 0))])
 
         root = i3 / f2
         possibilities = match_divide_numerics(root)
         self.assertEqualPos(possibilities,
-                [P(root, divide_numerics, (3, 2.0))])
+                [P(root, divide_numerics, (3, 2.0, 0))])
 
         root = f3 / f2
         possibilities = match_divide_numerics(root)
         self.assertEqualPos(possibilities,
-                [P(root, divide_numerics, (3.0, 2.0))])
+                [P(root, divide_numerics, (3.0, 2.0, 0))])
 
         root = i3 / f1
         possibilities = match_divide_numerics(root)
         self.assertEqualPos(possibilities,
-                [P(root, divide_numerics, (3, 1))])
+                [P(root, divide_numerics, (3, 1, 0))])
 
         root = a / b
         possibilities = match_divide_numerics(root)
@@ -61,55 +87,81 @@ class TestRulesNumerics(RulesTestCase):
     def test_divide_numerics(self):
         i2, i3, i6, f2, f3 = tree('2,3,6,2.0,3.0')
 
-        self.assertEqual(divide_numerics(i6 / i2, (6, 2)), 3)
-        self.assertEqual(divide_numerics(f3 / i2, (3.0, 2)), 1.5)
-        self.assertEqual(divide_numerics(i3 / f2, (3, 2.0)), 1.5)
-        self.assertEqual(divide_numerics(f3 / f2, (3.0, 2.0)), 1.5)
+        self.assertEqual(divide_numerics(i6 / i2, (6, 2, 0)), 3)
+        self.assertEqual(divide_numerics(f3 / i2, (3.0, 2, 0)), 1.5)
+        self.assertEqual(divide_numerics(i3 / f2, (3, 2.0, 0)), 1.5)
+        self.assertEqual(divide_numerics(f3 / f2, (3.0, 2.0, 0)), 1.5)
+
+        self.assertEqual(divide_numerics(i6 / i2, (6, 2, 1)), -3)
+        self.assertEqual(divide_numerics(i6 / i2, (6, 2, 2)), --i3)
+
+    def test_reduce_fraction_constants(self):
+        l1, l2 = tree('1,2')
+        self.assertEqual(reduce_fraction_constants(l2 / 4, (2,)), l1 / l2)
+
+    def test_fraction_to_int_fraction(self):
+        l1, l4 = tree('1,4')
+        self.assertEqual(fraction_to_int_fraction(l4 / 3, (1, 1, 3)),
+                         l1 + l1 / 3)
 
     def test_match_multiply_numerics(self):
         i2, i3, i6, f2, f3, f6 = tree('2,3,6,2.0,3.0,6.0')
 
         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)
+
+    def test_raise_numerics(self):
+        l1, l2 = root = tree('2 ^ 3')
+        self.assertEqualNodes(raise_numerics(root, (l1, l2)), L(8))
+
+        l1_neg, l2 = root = tree('(-2) ^ 2')
+        self.assertEqualNodes(raise_numerics(root, (l1_neg, l2)), --L(4))
+
+        l1_neg, l2 = root = tree('(-2) ^ 3')
+        self.assertEqualNodes(raise_numerics(root, (l1_neg, l2)), ---L(8))

+ 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)

+ 62 - 13
tests/test_rules_powers.py

@@ -2,10 +2,12 @@ from src.rules.powers import match_add_exponents, add_exponents, \
         match_subtract_exponents, subtract_exponents, \
         match_multiply_exponents, multiply_exponents, \
         match_duplicate_exponent, duplicate_exponent, \
+        match_raised_fraction, raised_fraction, \
         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 +19,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 +27,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 +37,24 @@ 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_add_exponents_negated(self):
+        a, q = tree('a,q')
+        n0, n1 = root = (-a) * a ** q
+
+        possibilities = match_add_exponents(root)
+        self.assertEqualPos(possibilities,
+                [P(root, add_exponents, (Scope(root), n0, n1, a, 1, q))])
 
     def test_match_subtract_exponents_powers(self):
         a, p, q = tree('a,p,q')
@@ -78,13 +96,25 @@ class TestRulesPowers(RulesTestCase):
         self.assertEqualPos(possibilities,
                 [P(root, duplicate_exponent, ([a, b], p))])
 
+    def test_match_raised_fraction(self):
+        ab, p = root = tree('(a / b) ^ p')
+
+        self.assertEqualPos(match_raised_fraction(root),
+                [P(root, raised_fraction, (ab, p))])
+
+    def test_raised_fraction(self):
+        ab, p = root = tree('(a / b) ^ p')
+        a, b = ab
+
+        self.assertEqual(raised_fraction(root, (ab, p)), a ** p / b ** p)
+
     def test_match_remove_negative_exponent(self):
         a, p = tree('a,p')
         root = a ** -p
 
         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 +133,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 +162,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 +177,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])

+ 18 - 0
tests/test_rules_sort.py

@@ -0,0 +1,18 @@
+from src.rules.sort import match_sort_multiplicants, move_constant
+from src.node import Scope
+from src.possibilities import Possibility as P
+from tests.rulestestcase import RulesTestCase, tree
+
+
+class TestRulesSort(RulesTestCase):
+
+    def test_match_sort_multiplicants(self):
+        x, l2 = root = tree('x * 2')
+        possibilities = match_sort_multiplicants(root)
+        self.assertEqualPos(possibilities,
+                [P(root, move_constant, (Scope(root), l2, x))])
+
+    def test_move_constant(self):
+        x, l2 = root = tree('x * 2')
+        self.assertEqualNodes(move_constant(root, (Scope(root), l2, x)),
+                              l2 * x)

+ 14 - 1
tests/test_rules_utils.py

@@ -1,6 +1,7 @@
 import unittest
 
-from src.rules.utils import least_common_multiple
+from src.rules.utils import least_common_multiple, is_fraction, partition
+from tests.rulestestcase import tree
 
 
 class TestRulesUtils(unittest.TestCase):
@@ -9,3 +10,15 @@ class TestRulesUtils(unittest.TestCase):
         self.assertEqual(least_common_multiple(5, 6), 30)
         self.assertEqual(least_common_multiple(5, 6, 15), 30)
         self.assertEqual(least_common_multiple(2, 4), 4)
+
+    def test_is_fraction(self):
+        l1, a = tree('1, a')
+
+        self.assertTrue(is_fraction(a / 2, a, 2))
+        self.assertTrue(is_fraction(l1 / 2 * a, a, 2))
+        self.assertTrue(is_fraction(a * (l1 / 2), a, 2))
+        self.assertFalse(is_fraction(l1 / 3 * a, a, 2))
+
+    def test_partition(self):
+        self.assertEqual(partition(lambda x: x & 1, range(6)),
+                         ([1, 3, 5], [0, 2, 4]))

+ 21 - 0
tests/test_validation.py

@@ -0,0 +1,21 @@
+from unittest import TestCase
+from src.validation import validate
+
+
+class TestValidation(TestCase):
+
+    def test_simple_success(self):
+        self.assertTrue(validate('3a+a', '4a'))
+
+    def test_simple_failure(self):
+        self.assertFalse(validate('3a+a', '4a+1'))
+
+    def test_intermediate_success(self):
+        self.assertTrue(validate('3a+a+b+2b', '4a+3b'))
+        self.assertTrue(validate('a/b/(c/d)', 'ad/(bc)'))
+
+    def test_intermediate_failure(self):
+        self.assertFalse(validate('3a+a+b+2b', '4a+4b'))
+
+    #def test_advanced_failure(self):
+    #    self.assertFalse(validate('(x-1)^3+(x-1)^3', '4a+4b'))