Эх сурвалжийг харах

Merge branch 'master' of kompiler.org:trs

Sander Mathijs van Veen 14 жил өмнө
parent
commit
2bc6587617

+ 56 - 39
TODO

@@ -1,48 +1,12 @@
+# vim: set fileencoding=utf-8 :
+
  - Fix BisonSyntaxError location tracking.
  - Fix BisonSyntaxError location tracking.
 
 
  - Sort polynom by its exponents?
  - 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.
  - 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
 smvv@multivac ~/work/trs $ printf "a/0\n??" | ./main.py
 Traceback (most recent call last):
 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)>
 <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.
 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.line import generate_line
 from graph_drawing.node import Node, Leaf
 from graph_drawing.node import Node, Leaf
 
 
+from unicode_math import PI as u_PI
+
 
 
 TYPE_OPERATOR = 1
 TYPE_OPERATOR = 1
 TYPE_IDENTIFIER = 2
 TYPE_IDENTIFIER = 2
@@ -29,9 +31,24 @@ OP_MOD = 7
 
 
 # N-ary (functions)
 # N-ary (functions)
 OP_INT = 8
 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 = {
 TYPE_MAP = {
@@ -41,17 +58,43 @@ TYPE_MAP = {
         }
         }
 
 
 OP_MAP = {
 OP_MAP = {
+        ',': OP_COMMA,
         '+': OP_ADD,
         '+': 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_MUL,
         '/': OP_DIV,
         '/': OP_DIV,
         '^': OP_POW,
         '^': OP_POW,
-        'mod': OP_MOD,
-        'int': OP_INT,
-        'expand': OP_EXPAND,
+        'sin': OP_SIN,
+        'cos': OP_COS,
+        'tan': OP_TAN,
         'sqrt': OP_SQRT,
         '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):
 class ExpressionBase(object):
+
+    def __init__(self, *args, **kwargs):
+        self.negated = 0
+
     def clone(self):
     def clone(self):
         return copy.deepcopy(self)
         return copy.deepcopy(self)
 
 
@@ -86,16 +133,11 @@ class ExpressionBase(object):
         if self.is_leaf:
         if self.is_leaf:
             if other.is_leaf:
             if other.is_leaf:
                 # Both are leafs, string compare the value.
                 # 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.
             # Self is a leaf, thus has less value than an expression node.
             return True
             return True
 
 
@@ -113,26 +155,11 @@ class ExpressionBase(object):
     def is_op(self, op):
     def is_op(self, op):
         return not self.is_leaf and self.op == 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
             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):
     def is_nary(self):
         return not self.is_leaf and self.op in [OP_ADD, OP_SUB, OP_MUL]
         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):
     def __pow__(self, other):
         return ExpressionNode('^', self, to_expression(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):
 class ExpressionNode(Node, ExpressionBase):
@@ -174,9 +211,6 @@ class ExpressionNode(Node, ExpressionBase):
         self.type = TYPE_OPERATOR
         self.type = TYPE_OPERATOR
         self.op = OP_MAP[args[0]]
         self.op = OP_MAP[args[0]]
 
 
-        if hasattr(self.op, '__call__'):
-            self.op = self.op(args)
-
     def __str__(self):  # pragma: nocover
     def __str__(self):  # pragma: nocover
         return generate_line(self)
         return generate_line(self)
 
 
@@ -184,10 +218,8 @@ class ExpressionNode(Node, ExpressionBase):
         """
         """
         Check strict equivalence.
         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):
     def substitute(self, old_child, new_child):
         self.nodes[self.nodes.index(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])
             return (ExpressionLeaf(1), self[0], self[1])
 
 
         # rule: -r -> (1, r, 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:
         if self.op != OP_MUL:
             return
             return
@@ -257,7 +291,7 @@ class ExpressionNode(Node, ExpressionBase):
             return (self[0], self[1], ExpressionLeaf(1))
             return (self[0], self[1], ExpressionLeaf(1))
         return (self[1], self[0], 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:
         Perform a non-strict equivalence check between two nodes:
         - If the other node is a leaf, it cannot be equal to this node.
         - 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
         - If both nodes are divisions, the nominator and denominator have to be
           non-strictly equal.
           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
             return False
 
 
         if self.op in (OP_ADD, OP_MUL):
         if self.op in (OP_ADD, OP_MUL):
             s0 = Scope(self)
             s0 = Scope(self)
             s1 = set(Scope(other))
             s1 = set(Scope(other))
 
 
-            # Scopes sould be of equal size
+            # Scopes should be of equal size
             if len(s0) != len(s1):
             if len(s0) != len(s1):
                 return False
                 return False
 
 
@@ -303,13 +333,15 @@ class ExpressionNode(Node, ExpressionBase):
                 if not child.equals(other[i]):
                 if not child.equals(other[i]):
                     return False
                     return False
 
 
-        return True
+        if ignore_negation:
+            return True
+
+        return self.negated == other.negated
 
 
 
 
 class ExpressionLeaf(Leaf, ExpressionBase):
 class ExpressionLeaf(Leaf, ExpressionBase):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super(ExpressionLeaf, self).__init__(*args, **kwargs)
         super(ExpressionLeaf, self).__init__(*args, **kwargs)
-
         self.type = TYPE_MAP[type(args[0])]
         self.type = TYPE_MAP[type(args[0])]
 
 
     def __eq__(self, other):
     def __eq__(self, other):
@@ -319,16 +351,42 @@ class ExpressionLeaf(Leaf, ExpressionBase):
         other_type = type(other)
         other_type = type(other)
 
 
         if other_type in TYPE_MAP:
         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.
         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):
     def extract_polynome_properties(self):
         """
         """
@@ -339,6 +397,11 @@ class ExpressionLeaf(Leaf, ExpressionBase):
         # rule: 1 * r ^ 1 -> (1, r, 1)
         # rule: 1 * r ^ 1 -> (1, r, 1)
         return (ExpressionLeaf(1), self, ExpressionLeaf(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):
 class Scope(object):
 
 
@@ -358,7 +421,14 @@ class Scope(object):
     def __iter__(self):
     def __iter__(self):
         return iter(self.nodes)
         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:
         if node.is_leaf:
             node_cmp = hash(node)
             node_cmp = hash(node)
         else:
         else:
@@ -371,8 +441,8 @@ class Scope(object):
                 n_cmp = n
                 n_cmp = n
 
 
             if n_cmp == node_cmp:
             if n_cmp == node_cmp:
-                if replacement != None:
-                    self[i] = replacement
+                if 'replacement' in kwargs:
+                    self[i] = kwargs['replacement']
                 else:
                 else:
                     del self.nodes[i]
                     del self.nodes[i]
 
 
@@ -381,8 +451,11 @@ class Scope(object):
         raise ValueError('Node "%s" is not in the scope of "%s".'
         raise ValueError('Node "%s" is not in the scope of "%s".'
                          % (node, self.node))
                          % (node, self.node))
 
 
+    def replace(self, node, replacement):
+        self.remove(node, replacement=replacement)
+
     def as_nary_node(self):
     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):
 def nary_node(operator, scope):
@@ -409,3 +482,13 @@ def get_scope(node):
             scope.append(child)
             scope.append(child)
 
 
     return scope
     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.
 file for the supported mathematical expressions.
 """
 """
 
 
-from node import ExpressionNode as Node, ExpressionLeaf as Leaf
-
 import os.path
 import os.path
 PYBISON_BUILD = os.path.realpath('build/external/pybison')
 PYBISON_BUILD = os.path.realpath('build/external/pybison')
 EXTERNAL_MODS = os.path.realpath('external')
 EXTERNAL_MODS = os.path.realpath('external')
@@ -16,9 +14,11 @@ sys.path.insert(1, EXTERNAL_MODS)
 from pybison import BisonParser, BisonSyntaxError
 from pybison import BisonParser, BisonSyntaxError
 from graph_drawing.graph import generate_graph
 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 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
 import Queue
 
 
@@ -44,6 +44,10 @@ class Parser(BisonParser):
     docstrings. Scanner rules are in the 'lexscript' attribute.
     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.
     # Output directory of generated pybison files, including a trailing slash.
     buildDirectory = PYBISON_BUILD + '/'
     buildDirectory = PYBISON_BUILD + '/'
 
 
@@ -52,10 +56,9 @@ class Parser(BisonParser):
     # ----------------------------------------------------------------
     # ----------------------------------------------------------------
     # TODO: add a runtime check to verify that this token list match the list
     # TODO: add a runtime check to verify that this token list match the list
     # of tokens of the lex script.
     # 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
     # precedences
@@ -64,8 +67,11 @@ class Parser(BisonParser):
         ('left', ('COMMA', )),
         ('left', ('COMMA', )),
         ('left', ('MINUS', 'PLUS')),
         ('left', ('MINUS', 'PLUS')),
         ('left', ('TIMES', 'DIVIDE')),
         ('left', ('TIMES', 'DIVIDE')),
+        ('left', ('EQ', )),
         ('left', ('NEG', )),
         ('left', ('NEG', )),
         ('right', ('POW', )),
         ('right', ('POW', )),
+        ('right', ('FUNCTION', )),
+        #('right', ('SIN', 'COS', 'TAN', 'SOLVE', 'INT', 'SQRT')),
         )
         )
 
 
     interactive = 0
     interactive = 0
@@ -74,13 +80,20 @@ class Parser(BisonParser):
         BisonParser.__init__(self, **kwargs)
         BisonParser.__init__(self, **kwargs)
         self.interactive = kwargs.get('interactive', 0)
         self.interactive = kwargs.get('interactive', 0)
         self.timeout = kwargs.get('timeout', 0)
         self.timeout = kwargs.get('timeout', 0)
-        self.possibilities = self.last_possibilities = []
 
 
+        self.reset()
+
+    def reset(self):
         self.read_buffer = ''
         self.read_buffer = ''
         self.read_queue = Queue.Queue()
         self.read_queue = Queue.Queue()
 
 
-        self.subtree_map = {}
+        #self.subtree_map = {}
         self.root_node = None
         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.
     # Override default read method with a version that prompts for input.
     def read(self, nbytes):
     def read(self, nbytes):
@@ -106,13 +119,13 @@ class Parser(BisonParser):
 
 
     def hook_read_before(self):
     def hook_read_before(self):
         if self.possibilities:
         if self.possibilities:
-            if self.interactive:  # pragma: nocover
+            if self.verbose:  # pragma: nocover
                 print 'possibilities:'
                 print 'possibilities:'
 
 
             items = filter_duplicates(self.possibilities)
             items = filter_duplicates(self.possibilities)
             self.last_possibilities = self.possibilities
             self.last_possibilities = self.possibilities
 
 
-            if self.interactive:  # pragma: nocover
+            if self.verbose:  # pragma: nocover
                 print '  ' + '\n  '.join(map(str, items))
                 print '  ' + '\n  '.join(map(str, items))
 
 
     def hook_read_after(self, data):
     def hook_read_after(self, data):
@@ -150,7 +163,7 @@ class Parser(BisonParser):
             left, right = filter(None, match.groups())
             left, right = filter(None, match.groups())
 
 
             # Filter words (otherwise they will be preprocessed as well)
             # Filter words (otherwise they will be preprocessed as well)
-            if left + right in ['graph', 'raise']:
+            if left + right in Parser.words:
                 return left + right
                 return left + right
 
 
             # If all characters on the right are numbers. e.g. "a4", the
             # 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")
             # match: ab | abc | abcd (where left = "a")
             return '*'.join([left] + list(right))
             return '*'.join([left] + list(right))
 
 
+        if self.verbose:  # pragma: nocover
+            data_before = data
+
         # Iteratively replace all matches.
         # Iteratively replace all matches.
         while True:
         while True:
             data_after = re.sub(pattern, preprocess_data, data)
             data_after = re.sub(pattern, preprocess_data, data)
@@ -170,41 +186,32 @@ class Parser(BisonParser):
             if data == data_after:
             if data == data_after:
                 break
                 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
             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
         return data
 
 
     def hook_handler(self, target, option, names, values, retval):
     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
             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
             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)
             self.possibilities.extend(possibilities)
 
 
         return retval
         return retval
@@ -213,7 +220,8 @@ class Parser(BisonParser):
         print pick_suggestion(self.last_possibilities)
         print pick_suggestion(self.last_possibilities)
 
 
     def display_possibilities(self):
     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):
     def rewrite(self):
         suggestion = pick_suggestion(self.last_possibilities)
         suggestion = pick_suggestion(self.last_possibilities)
@@ -224,8 +232,7 @@ class Parser(BisonParser):
         if not suggestion:
         if not suggestion:
             return self.root_node
             return self.root_node
 
 
-        expression = apply_suggestion(self.root_node, self.subtree_map,
-                                    suggestion)
+        expression = apply_suggestion(self.root_node, suggestion)
 
 
         if self.verbose:
         if self.verbose:
             print 'After application, expression=', expression
             print 'After application, expression=', expression
@@ -254,6 +261,7 @@ class Parser(BisonParser):
         """
         """
         input :
         input :
               | input line
               | input line
+              | input REWRITE NEWLINE
         """
         """
         if option == 1:
         if option == 1:
             # Interactive mode is enabled if the term rewriting system is used
             # Interactive mode is enabled if the term rewriting system is used
@@ -264,6 +272,10 @@ class Parser(BisonParser):
 
 
             return values[1]
             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):
     def on_line(self, target, option, names, values):
         """
         """
         line : NEWLINE
         line : NEWLINE
@@ -271,11 +283,17 @@ class Parser(BisonParser):
              | debug NEWLINE
              | debug NEWLINE
              | HINT NEWLINE
              | HINT NEWLINE
              | POSSIBILITIES NEWLINE
              | POSSIBILITIES NEWLINE
-             | REWRITE NEWLINE
              | RAISE NEWLINE
              | RAISE NEWLINE
         """
         """
         if option == 1:  # rule: EXP NEWLINE
         if option == 1:  # rule: EXP NEWLINE
             self.root_node = values[0]
             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]
             return values[0]
 
 
         if option == 2:  # rule: DEBUG NEWLINE
         if option == 2:  # rule: DEBUG NEWLINE
@@ -290,11 +308,7 @@ class Parser(BisonParser):
             self.display_possibilities()
             self.display_possibilities()
             return
             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')
             raise RuntimeError('on_line: exception raised')
 
 
     def on_debug(self, target, option, names, values):
     def on_debug(self, target, option, names, values):
@@ -340,10 +354,25 @@ class Parser(BisonParser):
     def on_unary(self, target, option, names, values):
     def on_unary(self, target, option, names, values):
         """
         """
         unary : MINUS exp %prec NEG
         unary : MINUS exp %prec NEG
+              | FUNCTION exp
         """
         """
 
 
         if option == 0:  # rule: NEG 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".'
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
                                % (option, target))  # pragma: nocover
                                % (option, target))  # pragma: nocover
@@ -354,19 +383,27 @@ class Parser(BisonParser):
                | exp TIMES exp
                | exp TIMES exp
                | exp DIVIDE exp
                | exp DIVIDE exp
                | exp POW exp
                | exp POW exp
+               | exp EQ exp
                | exp MINUS 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])
             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".'
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
                                % (option, target))  # pragma: nocover
                                % (option, target))  # pragma: nocover
@@ -382,6 +419,25 @@ class Parser(BisonParser):
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
                                % (option, target))  # pragma: nocover
                                % (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
     # raw lex script, verbatim here
     # -----------------------------------------
     # -----------------------------------------
@@ -409,8 +465,6 @@ class Parser(BisonParser):
             yylloc.first_column = yycolumn; \
             yylloc.first_column = yycolumn; \
             yylloc.last_column = yycolumn + yyleng; \
             yylloc.last_column = yycolumn + yyleng; \
             yycolumn += yyleng;
             yycolumn += yyleng;
-
-    /*[a-zA-Z][0-9]+ { returntoken(CONCAT_POW); }*/
     %}
     %}
 
 
     %option yylineno
     %option yylineno
@@ -421,19 +475,10 @@ class Parser(BisonParser):
     [a-zA-Z]  { returntoken(IDENTIFIER); }
     [a-zA-Z]  { returntoken(IDENTIFIER); }
     "("       { returntoken(LPAREN); }
     "("       { returntoken(LPAREN); }
     ")"       { returntoken(RPAREN); }
     ")"       { 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); }
     "raise"   { returntoken(RAISE); }
     "graph"   { returntoken(GRAPH); }
     "graph"   { returntoken(GRAPH); }
-    "sqrt"    { returntoken(SQRT); }
+    "quit"    { yyterminate(); returntoken(QUIT); }
 
 
     [ \t\v\f] { }
     [ \t\v\f] { }
     [\n]      { yycolumn = 0; returntoken(NEWLINE); }
     [\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
 # 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
 # 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
 # corresponding value is a string, which will be used to produce the hint
@@ -51,16 +54,27 @@ def filter_duplicates(possibilities):
     return unique
     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
     # 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
     # node, the subtree_map cannot be used since the hash() of each node in the
     # deep copied root node has changed.
     # deep copied root node has changed.
@@ -68,10 +82,7 @@ def apply_suggestion(root, subtree_map, suggestion):
 
 
     subtree = suggestion.handler(suggestion.root, suggestion.args)
     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.
     # 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
     # 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:
     if parent_node:
         parent_node.substitute(suggestion.root, subtree)
         parent_node.substitute(suggestion.root, subtree)
         return root
         return root
+
     return subtree
     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 .groups import match_combine_groups
 from .factors import match_expand
 from .factors import match_expand
 from .powers import match_add_exponents, match_subtract_exponents, \
 from .powers import match_add_exponents, match_subtract_exponents, \
         match_multiply_exponents, match_duplicate_exponent, \
         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, \
 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 = {
 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 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 ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 from ..translate import _
 
 
@@ -18,9 +18,13 @@ def match_expand(node):
     additions = []
     additions = []
 
 
     for n in Scope(node):
     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)
             leaves.append(n)
         elif n.op == OP_ADD:
         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)
             additions.append(n)
 
 
     for args in product(leaves, additions):
     for args in product(leaves, additions):
@@ -45,7 +49,7 @@ def expand_single(root, args):
     scope = Scope(root)
     scope = Scope(root)
 
 
     # Replace 'a' with the new expression
     # Replace 'a' with the new expression
-    scope.remove(a, a * b + a * c)
+    scope.replace(a, a * b + a * c)
 
 
     # Remove the addition
     # Remove the addition
     scope.remove(bc)
     scope.remove(bc)
@@ -66,7 +70,7 @@ def expand_double(root, args):
     scope = Scope(root)
     scope = Scope(root)
 
 
     # Replace 'a + b' with the new expression
     # 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
     # Remove the right addition
     scope.remove(cd)
     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 ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 from ..translate import _
 
 
@@ -44,7 +45,7 @@ def division_by_one(root, args):
     return args[0]
     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):
 def division_of_zero(root, args):
@@ -64,38 +65,30 @@ def division_by_self(root, args):
     return L(1)
     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):
 def match_add_constant_fractions(node):
     """
     """
     1 / 2 + 3 / 4  ->  2 / 4 + 3 / 4  # Equalize denominators
     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
     2 / 4 + 3 / 4  ->  5 / 4          # Equal denominators, so nominators can
                                       # be added
                                       # 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)
     assert node.is_op(OP_ADD)
 
 
     p = []
     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):
     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:
         if da == db:
             # Equal denominators, add nominators to create a single fraction
             # 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
             # least common multiple of their denominators. Later, the
             # nominators will be added
             # nominators will be added
             denom = least_common_multiple(da.value, db.value)
             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
     return p
 
 
@@ -113,29 +111,28 @@ def match_add_constant_fractions(node):
 def equalize_denominators(root, args):
 def equalize_denominators(root, args):
     """
     """
     1 / 2 + 3 / 4  ->  2 / 4 + 3 / 4
     1 / 2 + 3 / 4  ->  2 / 4 + 3 / 4
+    1 / 2 - 3 / 4  ->  2 / 4 - 3 / 4
     a / 2 + b / 4  ->  2a / 4 + b / 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
         mult = denom / d.value
 
 
         if mult != 1:
         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:
             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()
     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):
 def add_nominators(root, args):
@@ -147,21 +144,11 @@ def add_nominators(root, args):
     """
     """
     # TODO: is 'add' Appropriate when rewriting to "(a + (-c)) / b"?
     # TODO: is 'add' Appropriate when rewriting to "(a + (-c)) / b"?
     ab, cb = args
     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)
     scope = Scope(root)
 
 
     # Replace the left node with the new expression
     # 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
     # Remove the right node
     scope.remove(cb)
     scope.remove(cb)
@@ -185,3 +172,255 @@ def match_expand_and_add_fractions(node):
     p = []
     p = []
 
 
     return 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 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 ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 from ..translate import _
 
 
@@ -18,50 +18,53 @@ def match_combine_groups(node):
     ab + 2ab  ->  3ab
     ab + 2ab  ->  3ab
     ab + ba   ->  2ab
     ab + ba   ->  2ab
     """
     """
-    # TODO: handle OP_NEG nodes
     assert node.is_op(OP_ADD)
     assert node.is_op(OP_ADD)
 
 
     p = []
     p = []
     groups = []
     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
         # Each number multiplication yields a group, multiple occurences of
         # the same group can be replaced by a single one
         # the same group can be replaced by a single one
         if n.is_op(OP_MUL):
         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:
                     if len(others) == 1:
                         g = others[0]
                         g = others[0]
                     else:
                     else:
-                        g = Node('*', *others)
+                        g = nary_node('*', others)
 
 
                     groups.append((sub_node, g, n))
                     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
     return p
 
 
 
 
 def combine_groups(root, args):
 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
     # Replace the left node with the new expression
-    scope.remove(n0, (c0 + c1) * g0)
+    scope.replace(n0, (c0 + c1) * g0)
 
 
     # Remove the right node
     # Remove the right node
     scope.remove(n1)
     scope.remove(n1)
@@ -70,4 +73,4 @@ def combine_groups(root, args):
 
 
 
 
 MESSAGES[combine_groups] = \
 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 ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 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):
 def match_negated_division(node):
@@ -92,33 +112,28 @@ def match_negated_division(node):
     assert node.is_op(OP_DIV)
     assert node.is_op(OP_DIV)
 
 
     a, b = node
     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 []
     return []
 
 
 
 
 def single_negated_division(root, args):
 def single_negated_division(root, args):
     """
     """
-    -a / b  ->  -(a / b)
-    a / -b  ->  -(a / b)
+    a / -b  ->  -a / b
     """
     """
     a, b = args
     a, b = args
 
 
     # FIXME: "-a/b" results in "-(a/b)", which will cause a loop.
     # FIXME: "-a/b" results in "-(a/b)", which will cause a loop.
 
 
-    return -(a / b)
+    return -a / b
 
 
 
 
 MESSAGES[single_negated_division] = \
 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):
 def double_negated_division(root, args):
@@ -127,8 +142,11 @@ def double_negated_division(root, args):
     """
     """
     a, b = root
     a, b = root
 
 
-    return a[0] / b[0]
+    return +a / +b
 
 
 
 
 MESSAGES[double_negated_division] = \
 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 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 ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 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.
     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   ->  1
     -2 + 3   ->  1
     -2 + -3  ->  -5
     -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
     # 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
     # Remove the right node
-    scope.remove(n1)
+    scope.remove(c1)
 
 
     return scope.as_nary_node()
     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):
 #def match_subtract_numerics(node):
@@ -59,23 +87,41 @@ def match_divide_numerics(node):
 
 
     Example:
     Example:
     6 / 2      ->  3
     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.0 / 2    ->  1.5
     3 / 2.0    ->  1.5
     3 / 2.0    ->  1.5
     3.0 / 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)
     assert node.is_op(OP_DIV)
 
 
     n, d = node
     n, d = node
     divide = False
     divide = False
-    dv = d.value
+    nv, dv = n.value, d.value
 
 
     if n.is_int() and d.is_int():
     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():
     elif n.is_numeric() and d.is_numeric():
         if d == 1.0:
         if d == 1.0:
             # 3 / 1.0  ->  3
             # 3 / 1.0  ->  3
@@ -84,14 +130,14 @@ def match_divide_numerics(node):
         # 3.0 / 2  ->  1.5
         # 3.0 / 2  ->  1.5
         # 3 / 2.0  ->  1.5
         # 3 / 2.0  ->  1.5
         # 3.0 / 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):
 def divide_numerics(root, args):
     """
     """
-    Combine two constants to a single constant in a division.
+    Combine two divided constants into a single constant.
 
 
     Examples:
     Examples:
     6 / 2      ->  3
     6 / 2      ->  3
@@ -100,14 +146,48 @@ def divide_numerics(root, args):
     3.0 / 2.0  ->  1.5
     3.0 / 2.0  ->  1.5
     3 / 1.0    ->  3
     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}.')
 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):
 def match_multiply_zero(node):
     """
     """
     a * 0    ->  0
     a * 0    ->  0
@@ -119,20 +199,12 @@ def match_multiply_zero(node):
     assert node.is_op(OP_MUL)
     assert node.is_op(OP_MUL)
 
 
     left, right = node
     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):
 def multiply_zero(root, args):
@@ -143,17 +215,48 @@ def multiply_zero(root, args):
     0 * -a   ->  -0
     0 * -a   ->  -0
     -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.')
 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):
 def match_multiply_numerics(node):
     """
     """
     3 * 2      ->  6
     3 * 2      ->  6
@@ -164,16 +267,11 @@ def match_multiply_numerics(node):
     assert node.is_op(OP_MUL)
     assert node.is_op(OP_MUL)
 
 
     p = []
     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
     return p
 
 
@@ -185,24 +283,46 @@ def multiply_numerics(root, args):
     Example:
     Example:
     2 * 3  ->  6
     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
     # 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
     # Remove the right node
-    scope.remove(n1)
+    scope.remove(c1)
 
 
     return scope.as_nary_node()
     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 itertools import combinations
 
 
 from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, \
 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 ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 from ..translate import _
 
 
@@ -12,21 +12,23 @@ def match_add_exponents(node):
     a * a^q    ->  a^(1 + q)
     a * a^q    ->  a^(1 + q)
     a^p * a    ->  a^(p + 1)
     a^p * a    ->  a^(p + 1)
     a * a      ->  a^(1 + 1)
     a * a      ->  a^(1 + 1)
+    -a * a^q   ->  -a^(1 + q)
     """
     """
     assert node.is_op(OP_MUL)
     assert node.is_op(OP_MUL)
 
 
     p = []
     p = []
     powers = {}
     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():
         if n.is_identifier():
-            s = n
+            s = negate(n, 0)
             exponent = L(1)
             exponent = L(1)
         elif n.is_op(OP_POW):
         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
             s, exponent = n
-        else:
+        else:  # pragma: nocover
             continue
             continue
 
 
         s_str = str(s)
         s_str = str(s)
@@ -41,7 +43,7 @@ def match_add_exponents(node):
         # create a single power with that root
         # create a single power with that root
         if len(occurrences) > 1:
         if len(occurrences) > 1:
             for (n0, e1, a0), (n1, e2, a1) in combinations(occurrences, 2):
             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
     return p
 
 
@@ -50,11 +52,12 @@ def add_exponents(root, args):
     """
     """
     a^p * a^q  ->  a^(p + q)
     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
     # 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
     # Remove the right node
     scope.remove(n1)
     scope.remove(n1)
@@ -62,7 +65,7 @@ def add_exponents(root, args):
     return scope.as_nary_node()
     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):
 def match_subtract_exponents(node):
@@ -91,6 +94,18 @@ def match_subtract_exponents(node):
     return []
     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):
 def match_multiply_exponents(node):
     """
     """
     (a^p)^q  ->  a^(pq)
     (a^p)^q  ->  a^(pq)
@@ -105,34 +120,102 @@ def match_multiply_exponents(node):
     return []
     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):
 def match_duplicate_exponent(node):
     """
     """
     (ab)^p  ->  a^p * b^p
     (ab)^p  ->  a^p * b^p
     """
     """
     assert node.is_op(OP_POW)
     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 []
     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):
 def match_remove_negative_exponent(node):
     """
     """
-    a^-p  ->  1 / a^p
+    a ^ -p  ->  1 / a ^ p
     """
     """
     assert node.is_op(OP_POW)
     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 []
     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):
 def match_exponent_to_root(node):
     """
     """
     a^(1 / m)  ->  sqrt(a, m)
     a^(1 / m)  ->  sqrt(a, m)
@@ -148,6 +231,16 @@ def match_exponent_to_root(node):
     return []
     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):
 def match_extend_exponent(node):
     """
     """
     (a + ... + z)^n -> (a + ... + z)(a + ... + z)^(n - 1)  # n > 1
     (a + ... + z)^n -> (a + ... + z)(a + ... + z)^(n - 1)  # n > 1
@@ -176,64 +269,38 @@ def extend_exponent(root, args):
     return left * left
     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:
     while b:
         a, b = b, a % b
         a, b = b, a % b
@@ -12,7 +15,7 @@ def lcm(a, b):
     """
     """
     Return least common multiple of a and 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):
 def least_common_multiple(*args):
@@ -20,3 +23,51 @@ def least_common_multiple(*args):
     Return lcm of args.
     Return lcm of args.
     """
     """
     return reduce(lcm, 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:
         try:
             for i, exp in enumerate(rewrite_chain[:-1]):
             for i, exp in enumerate(rewrite_chain[:-1]):
                 self.assertMultiLineEqual(str(rewrite(exp)),
                 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
             raise

+ 3 - 3
tests/test_b1_ch08.py

@@ -11,13 +11,13 @@ class TestB1Ch08(unittest.TestCase):
         run_expressions(Parser, [
         run_expressions(Parser, [
             ('6*5^2', L(6) * L(5) ** 2),
             ('6*5^2', L(6) * L(5) ** 2),
             ('-5*(-3)^2', (-L(5)) * (-L(3)) ** 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))),
             ('-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):
     def test_diagnostic_test_application(self):
         apply_expressions(Parser, [
         apply_expressions(Parser, [
             ('7p+2p', 1, (L(7) + 2) * 'p'),
             ('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):
     def test_diagnostic_test(self):
         run_expressions(Parser, [
         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')),
             ('-(3a+6b)', -(L(3) * L('a') + L(6) * 'b')),
             ('18-(a-12)', L(18) + -(L('a') + -L(12))),
             ('18-(a-12)', L(18) + -(L('a') + -L(12))),
             ('-p-q+5(p-q)-3q-2(p-q)',
             ('-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',
             ('(2+3/7)^4',
                 N('^', N('+', L(2), N('/', L(3), L(7))), L(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))
                 == N('+', L(1), L(4))
 
 
     def test_basic_on_exp(self):
     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)),
                        ('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))),
                        ('3^4', N('^', L(3), L(4))),
                        ('(2)', L(2))]
                        ('(2)', L(2))]
 
 

+ 1 - 6
tests/test_exception.py

@@ -7,9 +7,4 @@ from tests.parser import ParserWrapper
 
 
 class TestException(unittest.TestCase):
 class TestException(unittest.TestCase):
     def test_raise(self):
     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):
 class TestLeidenOefenopgave(TestCase):
     def test_1_1(self):
     def test_1_1(self):
         for chain in [['-5(x2 - 3x + 6)', '-5(x ^ 2 - 3x) - 5 * 6',
         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)
             self.assertRewrite(chain)
@@ -15,80 +15,103 @@ class TestLeidenOefenopgave(TestCase):
         return
         return
 
 
         for exp, solution in [
         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'),
                 ('(4x + 5) * -(5 - 4x)', '16x^2 - 25'),
                 ]:
                 ]:
             self.assertEqual(str(rewrite(exp)), solution)
             self.assertEqual(str(rewrite(exp)), solution)
 
 
     def test_1_2(self):
     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):
     def test_1_3(self):
         # (x+1)^2 -> x^2 + 2x + 1
         # (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',
                        'xx + x * 1 + 1x + 1 * 1',
                        'x ^ (1 + 1) + 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 + (1 + 1)x + 1 * 1',
                        'x ^ 2 + 2x + 1 * 1',
                        'x ^ 2 + 2x + 1 * 1',
-                       'x ^ 2 + 2x + 1'],
-                     ]:
-            self.assertRewrite(chain)
+                       'x ^ 2 + 2x + 1',
+                       ])
 
 
     def test_1_4(self):
     def test_1_4(self):
         # (x-1)^2 -> x^2 - 2x + 1
         # (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):
     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):
     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):
     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):
     def test_1_5(self):
         self.assertRewrite(['(2x + x)x', '(2 + 1)xx', '3xx',
         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):
     def test_1_7(self):
         self.assertRewrite(['(4x + 5) * -(5 - 4x)',
         self.assertRewrite(['(4x + 5) * -(5 - 4x)',
@@ -97,15 +120,14 @@ class TestLeidenOefenopgave(TestCase):
                             '4x * -5 + 4x * 4x + 5 * -5 + 5 * 4x',
                             '4x * -5 + 4x * 4x + 5 * -5 + 5 * 4x',
                             '-20x + 4x * 4x + 5 * -5 + 5 * 4x',
                             '-20x + 4x * 4x + 5 * -5 + 5 * 4x',
                             '-20x + 16xx + 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):
     def test_2(self):
         pass
         pass
@@ -113,18 +135,29 @@ class TestLeidenOefenopgave(TestCase):
     def test_3(self):
     def test_3(self):
         pass
         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.assertTrue(N('+', *self.l[:2]).is_op(OP_ADD))
         self.assertFalse(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):
     def test_is_leaf(self):
         self.assertTrue(L(2).is_leaf)
         self.assertTrue(L(2).is_leaf)
         self.assertFalse(N('+', *self.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):
     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):
     def test_is_nary(self):
         self.assertTrue(N('+', *self.l[:2]).is_nary())
         self.assertTrue(N('+', *self.l[:2]).is_nary())
@@ -173,6 +165,13 @@ class TestNode(RulesTestCase):
         m0, m1 = tree('-5 * -3,-5 * 6')
         m0, m1 = tree('-5 * -3,-5 * 6')
         self.assertFalse(m0.equals(m1))
         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):
     def test_scope___init__(self):
         self.assertEqual(self.scope.node, self.n)
         self.assertEqual(self.scope.node, self.n)
         self.assertEqual(self.scope.nodes, [self.a, self.b, self.cd])
         self.assertEqual(self.scope.nodes, [self.a, self.b, self.cd])
@@ -185,14 +184,14 @@ class TestNode(RulesTestCase):
         self.scope.remove(self.cd)
         self.scope.remove(self.cd)
         self.assertEqual(self.scope.nodes, [self.a, self.b])
         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):
     def test_scope_remove_error(self):
         with self.assertRaises(ValueError):
         with self.assertRaises(ValueError):
             self.scope.remove(self.f)
             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):
     def test_nary_node(self):
         a, b, c, d = tree('a,b,c,d')
         a, b, c, d = tree('a,b,c,d')
 
 
@@ -205,3 +204,8 @@ class TestNode(RulesTestCase):
 
 
     def test_scope_as_nary_node(self):
     def test_scope_as_nary_node(self):
         self.assertEqualNodes(self.scope.as_nary_node(), self.n)
         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.parser import Parser
 from src.node import ExpressionNode as Node, ExpressionLeaf as Leaf
 from src.node import ExpressionNode as Node, ExpressionLeaf as Leaf
 from tests.parser import ParserWrapper, run_expressions, line, graph
 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):
 class TestParser(unittest.TestCase):
@@ -15,11 +17,42 @@ class TestParser(unittest.TestCase):
         run_expressions(Parser, [('a', Leaf('a'))])
         run_expressions(Parser, [('a', Leaf('a'))])
 
 
     def test_graph(self):
     def test_graph(self):
-        assert graph(Parser, '4a') == ("""
+        self.assertEqual(graph(Parser, '4a'), ("""
          *
          *
         ╭┴╮
         ╭┴╮
         4 a
         4 a
-        """).replace('\n        ', '\n')[1:-1]
+        """).replace('\n        ', '\n')[1:-1])
 
 
     def test_line(self):
     def test_line(self):
         self.assertEqual(line(Parser, '4-a'), '4 - a')
         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
         possibilities = parser.parser.possibilities
         self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
         self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
                     '<Possibility root="3 + 4" handler=add_numerics' \
                     '<Possibility root="3 + 4" handler=add_numerics' \
-                    ' args=(3, 4, 3, 4)>')
+                    ' args=(<Scope of "3 + 4">, 3, 4)>')
 
 
     def test_multiple_runs(self):
     def test_multiple_runs(self):
         parser = ParserWrapper(Parser)
         parser = ParserWrapper(Parser)
@@ -53,21 +53,19 @@ class TestPossibilities(unittest.TestCase):
         possibilities = parser.parser.possibilities
         possibilities = parser.parser.possibilities
         self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
         self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
                     '<Possibility root="1 + 2" handler=add_numerics' \
                     '<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(['', ' '])
         parser.run(['', ' '])
         possibilities = parser.parser.possibilities
         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
         # Overwrite previous possibilities with new ones
         parser.run(['3+4'])
         parser.run(['3+4'])
         possibilities = parser.parser.possibilities
         possibilities = parser.parser.possibilities
         self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
         self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
                     '<Possibility root="3 + 4" handler=add_numerics' \
                     '<Possibility root="3 + 4" handler=add_numerics' \
-                    ' args=(3, 4, 3, 4)>')
+                    ' args=(<Scope of "3 + 4">, 3, 4)>')
 
 
     def test_filter_duplicates(self):
     def test_filter_duplicates(self):
         a, b = ab = tree('a + b')
         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, \
 from src.rules.fractions import match_constant_division, division_by_one, \
         division_of_zero, division_by_self, match_add_constant_fractions, \
         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 src.possibilities import Possibility as P
 from tests.rulestestcase import RulesTestCase, tree
 from tests.rulestestcase import RulesTestCase, tree
 
 
@@ -51,12 +55,14 @@ class TestRulesFractions(RulesTestCase):
         n0, n1 = root = l1 / l2 + l3 / l4
         n0, n1 = root = l1 / l2 + l3 / l4
         possibilities = match_add_constant_fractions(root)
         possibilities = match_add_constant_fractions(root)
         self.assertEqualPos(possibilities,
         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
         (((n0, n1), n2), n3), n4 = root = a + l1 / l2 + b + l3 / l4 + c
         possibilities = match_add_constant_fractions(root)
         possibilities = match_add_constant_fractions(root)
         self.assertEqualPos(possibilities,
         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
         n0, n1 = root = l2 / l4 + l3 / l4
         possibilities = match_add_constant_fractions(root)
         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
         (((n0, n1), n2), n3), n4 = root = a + l2 / l2 + b + (-l3 / l4) + c
         possibilities = match_add_constant_fractions(root)
         possibilities = match_add_constant_fractions(root)
         self.assertEqualPos(possibilities,
         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
         (((n0, n1), n2), n3), n4 = root = a + l2 / l4 + b + (-l3 / l4) + c
         possibilities = match_add_constant_fractions(root)
         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')
         a, b, l1, l2, l3, l4 = tree('a,b,1,2,3,4')
 
 
         n0, n1 = root = l1 / l2 + l3 / l4
         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
         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
         #2 / 2 - 3 / 4  ->  4 / 4 - 3 / 4  # Equalize denominators
         n0, n1 = root = l1 / l2 + (-l3 / l4)
         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
         #2 / 2 - 3 / 4  ->  4 / 4 - 3 / 4  # Equalize denominators
         n0, n1 = root = a / l2 + (-b / 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))
 
 
     def test_add_nominators(self):
     def test_add_nominators(self):
         a, b, c = tree('a,b,c')
         a, b, c = tree('a,b,c')
@@ -118,3 +126,118 @@ class TestRulesFractions(RulesTestCase):
 
 
         n0, n1 = root = a / -b + -c / -b
         n0, n1 = root = a / -b + -c / -b
         self.assertEqualNodes(add_nominators(root, (n0, n1)), (a + -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.rules.groups import match_combine_groups, combine_groups
+from src.node import Scope
 from src.possibilities import Possibility as P
 from src.possibilities import Possibility as P
 from tests.rulestestcase import RulesTestCase, tree
 from tests.rulestestcase import RulesTestCase, tree
 
 
@@ -6,64 +7,88 @@ from tests.rulestestcase import RulesTestCase, tree
 class TestRulesGroups(RulesTestCase):
 class TestRulesGroups(RulesTestCase):
 
 
     def test_match_combine_groups_no_const(self):
     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)
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
         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):
     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
         l2, a1 = mul
 
 
         possibilities = match_combine_groups(root)
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
         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):
     def test_match_combine_groups_two_const(self):
         ((l2, a0), b), (l3, a1) = (m0, b), m1 = root = tree('2a + b + 3a')
         ((l2, a0), b), (l3, a1) = (m0, b), m1 = root = tree('2a + b + 3a')
 
 
         possibilities = match_combine_groups(root)
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
         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):
     def test_match_combine_groups_n_const(self):
         ((l2, a0), (l3, a1)), (l4, a2) = (m0, m1), m2 = root = tree('2a+3a+4a')
         ((l2, a0), (l3, a1)), (l4, a2) = (m0, m1), m2 = root = tree('2a+3a+4a')
 
 
         possibilities = match_combine_groups(root)
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
         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):
     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)
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
         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):
     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
         (l2, a), b = m1
 
 
         possibilities = match_combine_groups(root)
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
         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):
     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
         b, a = m1
 
 
         possibilities = match_combine_groups(root)
         possibilities = match_combine_groups(root)
         self.assertEqualPos(possibilities,
         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):
     def test_combine_groups_simple(self):
         root, l1 = tree('a + a,1')
         root, l1 = tree('a + a,1')
         a0, a1 = root
         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)
                               (l1 + 1) * a0)
 
 
     def test_combine_groups_nary(self):
     def test_combine_groups_nary(self):
@@ -71,5 +96,6 @@ class TestRulesGroups(RulesTestCase):
         abb, ba = root
         abb, ba = root
         ab, b = abb
         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)
                               (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 src.possibilities import Possibility as P
 from tests.rulestestcase import RulesTestCase, tree
 from tests.rulestestcase import RulesTestCase, tree
 
 
 
 
 class TestRulesNegation(RulesTestCase):
 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):
     def test_match_negated_division_none(self):
         self.assertEqual(match_negated_division(tree('1 / 2')), [])
         self.assertEqual(match_negated_division(tree('1 / 2')), [])
 
 
     def test_match_negated_division_single(self):
     def test_match_negated_division_single(self):
         l1, l2 = root = tree('-1 / 2')
         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')
         l1, l2 = root = tree('1 / -2')
         possibilities = match_negated_division(root)
         possibilities = match_negated_division(root)
         self.assertEqualPos(possibilities,
         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):
     def test_match_negated_division_double(self):
         root = tree('-1 / -2')
         root = tree('-1 / -2')
 
 
         possibilities = match_negated_division(root)
         possibilities = match_negated_division(root)
         self.assertEqualPos(possibilities,
         self.assertEqualPos(possibilities,
-                [P(root, double_negated_division, (root,))])
+                [P(root, double_negated_division, ())])
 
 
     def test_single_negated_division(self):
     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')
         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):
     def test_double_negated_division(self):
         l1, l2 = root = tree('-1 / -2')
         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.possibilities import Possibility as P
-from src.node import ExpressionLeaf as L
 from tests.rulestestcase import RulesTestCase, tree
 from tests.rulestestcase import RulesTestCase, tree
 
 
 
 
 class TestRulesNumerics(RulesTestCase):
 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):
     def test_add_numerics(self):
         l0, a, l1 = tree('1,a,2')
         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):
     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):
     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
         root = i6 / i2
         possibilities = match_divide_numerics(root)
         possibilities = match_divide_numerics(root)
         self.assertEqualPos(possibilities,
         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
         root = i3 / i2
         possibilities = match_divide_numerics(root)
         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
         root = f3 / i2
         possibilities = match_divide_numerics(root)
         possibilities = match_divide_numerics(root)
         self.assertEqualPos(possibilities,
         self.assertEqualPos(possibilities,
-                [P(root, divide_numerics, (3.0, 2))])
+                [P(root, divide_numerics, (3.0, 2, 0))])
 
 
         root = i3 / f2
         root = i3 / f2
         possibilities = match_divide_numerics(root)
         possibilities = match_divide_numerics(root)
         self.assertEqualPos(possibilities,
         self.assertEqualPos(possibilities,
-                [P(root, divide_numerics, (3, 2.0))])
+                [P(root, divide_numerics, (3, 2.0, 0))])
 
 
         root = f3 / f2
         root = f3 / f2
         possibilities = match_divide_numerics(root)
         possibilities = match_divide_numerics(root)
         self.assertEqualPos(possibilities,
         self.assertEqualPos(possibilities,
-                [P(root, divide_numerics, (3.0, 2.0))])
+                [P(root, divide_numerics, (3.0, 2.0, 0))])
 
 
         root = i3 / f1
         root = i3 / f1
         possibilities = match_divide_numerics(root)
         possibilities = match_divide_numerics(root)
         self.assertEqualPos(possibilities,
         self.assertEqualPos(possibilities,
-                [P(root, divide_numerics, (3, 1))])
+                [P(root, divide_numerics, (3, 1, 0))])
 
 
         root = a / b
         root = a / b
         possibilities = match_divide_numerics(root)
         possibilities = match_divide_numerics(root)
@@ -61,55 +87,81 @@ class TestRulesNumerics(RulesTestCase):
     def test_divide_numerics(self):
     def test_divide_numerics(self):
         i2, i3, i6, f2, f3 = tree('2,3,6,2.0,3.0')
         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):
     def test_match_multiply_numerics(self):
         i2, i3, i6, f2, f3, f6 = tree('2,3,6,2.0,3.0,6.0')
         i2, i3, i6, f2, f3, f6 = tree('2,3,6,2.0,3.0,6.0')
 
 
         root = i3 * i2
         root = i3 * i2
         self.assertEqual(match_multiply_numerics(root),
         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
         root = f3 * i2
         self.assertEqual(match_multiply_numerics(root),
         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
         root = i3 * f2
         self.assertEqual(match_multiply_numerics(root),
         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
         root = f3 * f2
         self.assertEqual(match_multiply_numerics(root),
         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):
     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')
         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):
     def test_multiply_numerics_negation(self):
         l1_neg, l2 = root = tree('-1 * 2')
         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
         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')
         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_subtract_exponents, subtract_exponents, \
         match_multiply_exponents, multiply_exponents, \
         match_multiply_exponents, multiply_exponents, \
         match_duplicate_exponent, duplicate_exponent, \
         match_duplicate_exponent, duplicate_exponent, \
+        match_raised_fraction, raised_fraction, \
         match_remove_negative_exponent, remove_negative_exponent, \
         match_remove_negative_exponent, remove_negative_exponent, \
-        match_exponent_to_root, exponent_to_root
+        match_exponent_to_root, exponent_to_root, \
+        match_constant_exponent, remove_power_of_zero, remove_power_of_one
+from src.node import Scope, ExpressionNode as N
 from src.possibilities import Possibility as P
 from src.possibilities import Possibility as P
-from src.node import ExpressionNode as N
 from tests.rulestestcase import RulesTestCase, tree
 from tests.rulestestcase import RulesTestCase, tree
 
 
 
 
@@ -17,7 +19,7 @@ class TestRulesPowers(RulesTestCase):
 
 
         possibilities = match_add_exponents(root)
         possibilities = match_add_exponents(root)
         self.assertEqualPos(possibilities,
         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):
     def test_match_add_exponents_ternary(self):
         a, p, q, r = tree('a,p,q,r')
         a, p, q, r = tree('a,p,q,r')
@@ -25,9 +27,9 @@ class TestRulesPowers(RulesTestCase):
 
 
         possibilities = match_add_exponents(root)
         possibilities = match_add_exponents(root)
         self.assertEqualPos(possibilities,
         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):
     def test_match_add_exponents_multiple_identifiers(self):
         a, b, p, q = tree('a,b,p,q')
         a, b, p, q = tree('a,b,p,q')
@@ -35,8 +37,24 @@ class TestRulesPowers(RulesTestCase):
 
 
         possibilities = match_add_exponents(root)
         possibilities = match_add_exponents(root)
         self.assertEqualPos(possibilities,
         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):
     def test_match_subtract_exponents_powers(self):
         a, p, q = tree('a,p,q')
         a, p, q = tree('a,p,q')
@@ -78,13 +96,25 @@ class TestRulesPowers(RulesTestCase):
         self.assertEqualPos(possibilities,
         self.assertEqualPos(possibilities,
                 [P(root, duplicate_exponent, ([a, b], p))])
                 [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):
     def test_match_remove_negative_exponent(self):
         a, p = tree('a,p')
         a, p = tree('a,p')
         root = a ** -p
         root = a ** -p
 
 
         possibilities = match_remove_negative_exponent(root)
         possibilities = match_remove_negative_exponent(root)
         self.assertEqualPos(possibilities,
         self.assertEqualPos(possibilities,
-                [P(root, remove_negative_exponent, (a, p))])
+                [P(root, remove_negative_exponent, (a, -p))])
 
 
     def test_match_exponent_to_root(self):
     def test_match_exponent_to_root(self):
         a, n, m, l1 = tree('a,n,m,1')
         a, n, m, l1 = tree('a,n,m,1')
@@ -103,7 +133,8 @@ class TestRulesPowers(RulesTestCase):
         a, p, q = tree('a,p,q')
         a, p, q = tree('a,p,q')
         n0, n1 = root = a ** p * a ** 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):
     def test_subtract_exponents(self):
         a, p, q = tree('a,p,q')
         a, p, q = tree('a,p,q')
@@ -131,11 +162,11 @@ class TestRulesPowers(RulesTestCase):
                               a ** p * b ** p * c ** p)
                               a ** p * b ** p * c ** p)
 
 
     def test_remove_negative_exponent(self):
     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)),
         self.assertEqualNodes(remove_negative_exponent(root, (a, p)),
-                              l1 / a ** p)
+                              l1 / a ** +p)
 
 
     def test_exponent_to_root(self):
     def test_exponent_to_root(self):
         a, n, m, l1 = tree('a,n,m,1')
         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)),
         self.assertEqualNodes(exponent_to_root(root, (a, l1, m)),
                               N('sqrt', a, 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
 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):
 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), 30)
         self.assertEqual(least_common_multiple(5, 6, 15), 30)
         self.assertEqual(least_common_multiple(5, 6, 15), 30)
         self.assertEqual(least_common_multiple(2, 4), 4)
         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'))