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

Merge branch 'master' into gonio

Conflicts:
	tests/test_rules_goniometry.py
Taddeus Kroes 14 жил өмнө
parent
commit
6139bd8225

+ 8 - 39
TODO

@@ -2,47 +2,9 @@
 
  - Sort polynom by its exponents?
 
- - No possibilities found for:
-   >>> a2b3 + a2b3
-   a ^ 2 * b ^ 3 + a ^ 2 * b ^ 3
-
- - 2 + 3 + 4 rewrites to 5 instead of 5 + 4
-   -> the problem is that the 'root' of the application is actually a subtree
-   of the entire expression. This means that the parent of each possibility
-   root (or 'subtree') must me stored to be able to replace the subtree.
-
  - MESSAGES needs to be expanded.
 
- - rewrite match_combine_polynomes to an even more generic form:
-   match_combine_factors.
-
- - "--ab + c" has no rewrite possibility. The graph of "--ab + c" is also
-   not valid:
-
-     -
-     │
-      +
-    ╭─┴╮
-    *  c
-   ╭┴╮
-   - b
-   │
-   a
-
- - The following expression gives a cycle in the possibilities:
-
-   >>> ab + ba
-   possibilities:
-     Group "ab" is multiplied by 1 and 1, combine them.
-   >>> (1 + 1) * ab
-   (1 + 1)ab
-   possibilities:
-     Combine the constants 1 and 1.
-     Group "1" is multiplied by 1 and 1, combine them.
-     Expand a(1 + 1).
-     Expand b(1 + 1).
-
- - Fix division by zero caused by "0/0".
+ - Fix division by zero caused by "0/0": Catch exception in front-end
 
 smvv@multivac ~/work/trs $ printf "a/0\n??" | ./main.py
 Traceback (most recent call last):
@@ -84,3 +46,10 @@ smvv@multivac ~/work/trs $ printf "0/1\n??" | ./main.py
 <Possibility root="0 / 1" handler=divide_numerics args=(0, 1)>
 Division of 0 by 1 reduces to 0.
 Division of 0 by 1 reduces to 0.
+
+ - Fractions constant rewrite rules.
+
+ - >>> (sin x) ^ 2 + (cos x) ^ 2
+   sin(x) ^ 2 + cos(x) ^ 2
+   >>> sin(x) ^ 2 + cos(x) ^ 2
+   sin(x ^ 2) + cos(x ^ 2)

+ 1 - 1
external/graph_drawing

@@ -1 +1 @@
-Subproject commit 821bdb8f8408fb36ee1d92ada162589794a8c5b3
+Subproject commit 21f0710c80184b6ea18827808ec40af924f0c8a8

+ 17 - 4
src/parser.py

@@ -61,8 +61,10 @@ class Parser(BisonParser):
         ('left', ('COMMA', )),
         ('left', ('MINUS', 'PLUS')),
         ('left', ('TIMES', 'DIVIDE')),
+        ('left', ('EQ', )),
         ('left', ('NEG', )),
         ('right', ('POW', )),
+        ('right', ('SIN', 'COS', 'TAN', 'SOLVE', 'INT', 'SQRT')),
         )
 
     interactive = 0
@@ -154,7 +156,7 @@ class Parser(BisonParser):
             left, right = filter(None, match.groups())
 
             # Filter words (otherwise they will be preprocessed as well)
-            if left + right in ['graph', 'raise']:
+            if (left + right).upper() in self.tokens:
                 return left + right
 
             # If all characters on the right are numbers. e.g. "a4", the
@@ -334,6 +336,12 @@ class Parser(BisonParser):
     def on_unary(self, target, option, names, values):
         """
         unary : MINUS exp %prec NEG
+              | SIN exp
+              | COS exp
+              | TAN exp
+              | INT exp
+              | SOLVE exp
+              | SQRT exp
         """
 
         if option == 0:  # rule: NEG exp
@@ -346,6 +354,11 @@ class Parser(BisonParser):
 
             return values[1]
 
+        if option < 7:  # rule: SIN exp | COS exp | TAN exp | INT exp
+            if values[1].type == TYPE_OPERATOR and values[1].op == OP_COMMA:
+                return Node(values[0], *values[1])
+            return Node(*values)
+
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
                                % (option, target))  # pragma: nocover
 
@@ -355,13 +368,14 @@ class Parser(BisonParser):
                | exp TIMES exp
                | exp DIVIDE exp
                | exp POW exp
+               | exp EQ exp
                | exp MINUS exp
         """
 
-        if 0 <= option < 4:  # rule: exp {PLUS,TIMES,DIVIDES,POW} exp
+        if 0 <= option < 5:  # rule: exp {PLUS,TIMES,DIVIDES,POW,EQ} exp
             return Node(values[1], values[0], values[2])
 
-        if option == 4:  # rule: exp MINUS exp
+        if option == 5:  # rule: exp MINUS exp
             node = values[2]
 
             # Add negation to the left-most child
@@ -436,7 +450,6 @@ class Parser(BisonParser):
     [a-zA-Z]  { returntoken(IDENTIFIER); }
     "("       { returntoken(LPAREN); }
     ")"       { returntoken(RPAREN); }
-    ","       { returntoken(COMMA); }
     """ + operators + r"""
     "raise"   { returntoken(RAISE); }
     "graph"   { returntoken(GRAPH); }

+ 3 - 1
src/rules/__init__.py

@@ -11,13 +11,15 @@ from .fractions import match_constant_division, match_add_constant_fractions, \
         match_expand_and_add_fractions
 from .negation import match_negated_factor, match_negate_polynome, \
         match_negated_division
+from .sort import match_sort_multiplicants
 
 RULES = {
         OP_ADD: [match_add_numerics, match_add_constant_fractions,
                  match_combine_groups],
         OP_MUL: [match_multiply_numerics, match_expand, match_add_exponents,
                  match_expand_and_add_fractions, match_multiply_zero,
-                 match_negated_factor, match_multiply_one],
+                 match_negated_factor, match_multiply_one,
+                 match_sort_multiplicants],
         OP_DIV: [match_subtract_exponents, match_divide_numerics,
                  match_constant_division, match_negated_division],
         OP_POW: [match_multiply_exponents, match_duplicate_exponent,

+ 14 - 2
src/rules/goniometry.py

@@ -1,9 +1,21 @@
-from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, \
-        OP_ADD, OP_POW, OP_MUL, OP_SIN, OP_COS, OP_TAN
+from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, OP_ADD, \
+        OP_POW, OP_MUL, OP_SIN, OP_COS, OP_TAN
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 
 
+def 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(x) ^ 2 + cos(x) ^ 2  ->  1

+ 2 - 2
src/rules/groups.py

@@ -1,7 +1,7 @@
 from itertools import combinations
 
-from ..node import ExpressionNode as Node, ExpressionLeaf as Leaf, Scope, \
-        OP_ADD, OP_MUL, nary_node, negate
+from ..node import ExpressionLeaf as Leaf, Scope, OP_ADD, OP_MUL, nary_node, \
+        negate
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 

+ 0 - 5
src/rules/numerics.py

@@ -38,11 +38,6 @@ def add_numerics(root, args):
     scope, c0, c1 = args
     value = c0.actual_value() + c1.actual_value()
 
-    if value < 0:
-        leaf = Leaf(-value).negate()
-    else:
-        leaf = Leaf(value)
-
     # Replace the left node with the new expression
     scope.replace(c0, Leaf(abs(value)).negate(int(value < 0)))
 

+ 8 - 5
src/rules/powers.py

@@ -1,7 +1,7 @@
 from itertools import combinations
 
 from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, \
-                   OP_MUL, OP_DIV, OP_POW, OP_ADD
+                   OP_MUL, OP_DIV, OP_POW, OP_ADD, negate
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 
@@ -12,6 +12,7 @@ def match_add_exponents(node):
     a * a^q    ->  a^(1 + q)
     a^p * a    ->  a^(p + 1)
     a * a      ->  a^(1 + 1)
+    -a * a^q   ->  -a^(1 + q)
     """
     assert node.is_op(OP_MUL)
 
@@ -20,12 +21,12 @@ def match_add_exponents(node):
     scope = Scope(node)
 
     for n in scope:
+        # Order powers by their roots, e.g. a^p and a^q are put in the same
+        # list because of the mutual 'a'
         if n.is_identifier():
-            s = n
+            s = negate(n, 0)
             exponent = L(1)
         elif n.is_op(OP_POW):
-            # Order powers by their roots, e.g. a^p and a^q are put in the same
-            # list because of the mutual 'a'
             s, exponent = n
         else:  # pragma: nocover
             continue
@@ -53,8 +54,10 @@ def add_exponents(root, args):
     """
     scope, n0, n1, a, p, q = args
 
+    # TODO: combine exponent negations
+
     # Replace the left node with the new expression
-    scope.replace(n0, a ** (p + q))
+    scope.replace(n0, (a ** (p + q)).negate(n0.negated + n1.negated))
 
     # Remove the right node
     scope.remove(n1)

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

+ 33 - 0
src/validation.py

@@ -0,0 +1,33 @@
+from src.parser import Parser
+from tests.parser import ParserWrapper
+
+
+class ValidationNode(object):
+    pass
+
+
+def validate(exp, result):
+    """
+    Validate that exp =>* result.
+    """
+    parser = ParserWrapper(Parser)
+
+    exp = parser.run([exp])
+    result = parser.run([result])
+
+    return validate_graph(exp, result)
+
+
+def iter_preorder(exp, possibility):
+    """
+    Traverse the possibility tree using pre-order traversal.
+    """
+    pass
+
+
+def validate_graph(exp, result):
+    """
+    Validate that "exp" =>* "result".
+    """
+    # TODO: Traverse the tree of possibility applications
+    return False

+ 32 - 31
tests/test_leiden_oefenopgave.py

@@ -4,10 +4,10 @@ from tests.rulestestcase import RulesTestCase as TestCase, rewrite
 class TestLeidenOefenopgave(TestCase):
     def test_1_1(self):
         for chain in [['-5(x2 - 3x + 6)', '-5(x ^ 2 - 3x) - 5 * 6',
-                       '-5 * x ^ 2 - 5 * -3x - 5 * 6',
-                       '-5 * x ^ 2 - -15x - 5 * 6',
-                       '-5 * x ^ 2 + 15x - 5 * 6',
-                       '-5 * x ^ 2 + 15x - 30',
+                       '-5x ^ 2 - 5 * -3x - 5 * 6',
+                       '-5x ^ 2 - -15x - 5 * 6',
+                       '-5x ^ 2 + 15x - 5 * 6',
+                       '-5x ^ 2 + 15x - 30',
                        ],
                      ]:
             self.assertRewrite(chain)
@@ -15,11 +15,11 @@ class TestLeidenOefenopgave(TestCase):
         return
 
         for exp, solution in [
-                ('-5(x2 - 3x + 6)',       '-30 + 15 * x - 5 * x ^ 2'),
-                ('(x+1)^2',              'x ^ 2 + 2 * x + 1'),
-                ('(x-1)^2',              'x ^ 2 - 2 * x + 1'),
-                ('(2x+x)*x',             '3 * x ^ 2'),
-                ('-2(6x-4)^2*x',         '-72 * x^3 + 96 * x ^ 2 + 32 * x'),
+                ('-5(x2 - 3x + 6)',       '-30 + 15x - 5x ^ 2'),
+                ('(x+1)^2',              'x ^ 2 + 2x + 1'),
+                ('(x-1)^2',              'x ^ 2 - 2x + 1'),
+                ('(2x+x)*x',             '3x ^ 2'),
+                ('-2(6x-4)^2*x',         '-72x ^ 3 + 96x ^ 2 + 32x'),
                 ('(4x + 5) * -(5 - 4x)', '16x^2 - 25'),
                 ]:
             self.assertEqual(str(rewrite(exp)), solution)
@@ -36,20 +36,21 @@ class TestLeidenOefenopgave(TestCase):
                 '(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',
+                '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 + 1 * x ^ 2 + 1 * 2x + 1x + 1 * 1',
-                'x ^ 3 + x ^ 2 * 2 + x ^ 2 + 1 * 2x + 1x + 1 * 1',
-                'x ^ 3 + (2 + 1) * x ^ 2 + 1 * 2x + 1x + 1 * 1',
-                'x ^ 3 + 3 * x ^ 2 + 1 * 2x + 1x + 1 * 1',
-                'x ^ 3 + 3 * x ^ 2 + 2x + 1x + 1 * 1',
-                'x ^ 3 + 3 * x ^ 2 + 2x + x + 1 * 1',
-                'x ^ 3 + 3 * x ^ 2 + (2 + 1)x + 1 * 1',
-                'x ^ 3 + 3 * x ^ 2 + 3x + 1 * 1',
-                'x ^ 3 + 3 * x ^ 2 + 3x + 1',
+                'x ^ 3 + 2xx + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
+                'x ^ 3 + 2x ^ (1 + 1) + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
+                'x ^ 3 + 2x ^ 2 + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
+                'x ^ 3 + 2x ^ 2 + 1x ^ 2 + 1 * 2x + 1x + 1 * 1',
+                'x ^ 3 + 2x ^ 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',
                 ]
             ]:
             self.assertRewrite(chain)
@@ -115,7 +116,7 @@ class TestLeidenOefenopgave(TestCase):
 
     def test_1_5(self):
         self.assertRewrite(['(2x + x)x', '(2 + 1)xx', '3xx',
-                            '3 * x ^ (1 + 1)', '3 * x ^ 2'])
+                            '3x ^ (1 + 1)', '3x ^ 2'])
 
     def test_1_7(self):
         self.assertRewrite(['(4x + 5) * -(5 - 4x)',
@@ -124,15 +125,15 @@ class TestLeidenOefenopgave(TestCase):
                             '4x * -5 + 4x * 4x + 5 * -5 + 5 * 4x',
                             '-20x + 4x * 4x + 5 * -5 + 5 * 4x',
                             '-20x + 16xx + 5 * -5 + 5 * 4x',
-                            '-20x + 16 * x ^ (1 + 1) + 5 * -5 + 5 * 4x',
-                            '-20x + 16 * x ^ 2 + 5 * -5 + 5 * 4x',
-                            '-20x + 16 * x ^ 2 - 25 + 5 * 4x',
-                            '-20x + 16 * x ^ 2 - 25 + 20x',
-                            '(-20 + 20)x + 16 * x ^ 2 - 25',
-                            '0x + 16 * x ^ 2 - 25',
-                            '0 + 16 * x ^ 2 - 25',
-                            '-25 + 16 * x ^ 2'])
-                            # FIXME: '16 * x ^ 2 - 25'])
+                            '-20x + 16x ^ (1 + 1) + 5 * -5 + 5 * 4x',
+                            '-20x + 16x ^ 2 + 5 * -5 + 5 * 4x',
+                            '-20x + 16x ^ 2 - 25 + 5 * 4x',
+                            '-20x + 16x ^ 2 - 25 + 20x',
+                            '(-20 + 20)x + 16x ^ 2 - 25',
+                            '0x + 16x ^ 2 - 25',
+                            '0 + 16x ^ 2 - 25',
+                            '-25 + 16x ^ 2'])
+                            # FIXME: '16x ^ 2 - 25'])
 
     def test_2(self):
         pass

+ 23 - 21
tests/test_leiden_oefenopgave_v12.py

@@ -4,7 +4,7 @@ from tests.rulestestcase import RulesTestCase as TestCase, rewrite
 class TestLeidenOefenopgaveV12(TestCase):
     def test_1_e(self):
         self.assertRewrite([
-            '-2(6x - 4) ^ 2 * x',
+            '-2(6x - 4) ^ 2x',
             '-2(6x - 4)(6x - 4)x',
             '(-2 * 6x - 2 * -4)(6x - 4)x',
             '(-12x - 2 * -4)(6x - 4)x',
@@ -12,23 +12,25 @@ class TestLeidenOefenopgaveV12(TestCase):
             '(-12x + 8)(6x - 4)x',
             '(-12x * 6x - 12x * -4 + 8 * 6x + 8 * -4)x',
             '(-72xx - 12x * -4 + 8 * 6x + 8 * -4)x',
-            '(-72 * x ^ (1 + 1) - 12x * -4 + 8 * 6x + 8 * -4)x',
-            '(-72 * x ^ 2 - 12x * -4 + 8 * 6x + 8 * -4)x',
-            '(-72 * x ^ 2 - -48x + 8 * 6x + 8 * -4)x',
-            '(-72 * x ^ 2 + 48x + 8 * 6x + 8 * -4)x',
-            '(-72 * x ^ 2 + 48x + 48x + 8 * -4)x',
-            '(-72 * x ^ 2 + (1 + 1) * 48x + 8 * -4)x',
-            '(-72 * x ^ 2 + 2 * 48x + 8 * -4)x',
-            '(-72 * x ^ 2 + 96x + 8 * -4)x',
-            '(-72 * x ^ 2 + 96x - 32)x',
-            'x(-72 * x ^ 2 + 96x) + x * -32',
-            'x * -72 * x ^ 2 + x * 96x + x * -32',
-            '-x * 72 * x ^ 2 + x * 96x + x * -32',
-            '-x * 72 * x ^ 2 + x ^ (1 + 1) * 96 + x * -32',
-            '-x * 72 * x ^ 2 + x ^ 2 * 96 + x * -32',
-            '-x * 72 * x ^ 2 + x ^ 2 * 96 - x * 32'])
-            # FIXME: '-x ^ (1 + 2) * 72 + x ^ 2 * 96 - x * 32',
-            # FIXME: '-x ^ 3 * 72 + x ^ 2 * 96 - x * 32',
-            # FIXME: '-72x ^ 3 + x ^ 2 * 96 - x * 32',
-            # FIXME: '-72x ^ 3 + 96x ^ 2 - x * 32',
-            # FIXME: '-72x ^ 3 + 96x ^ 2 - 32x'])
+            '(-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',
+            '72 * -xx ^ 2 + x * 96x + x * -32',
+            '-72xx ^ 2 + x * 96x + x * -32',
+            '-72x ^ (1 + 2) + x * 96x + x * -32',
+            '-72x ^ 3 + x * 96x + x * -32',
+            '-72x ^ 3 + 96xx + x * -32',
+            '-72x ^ 3 + 96x ^ (1 + 1) + x * -32',
+            '-72x ^ 3 + 96x ^ 2 + x * -32',
+            '-72x ^ 3 + 96x ^ 2 - x * 32',
+            '-72x ^ 3 + 96x ^ 2 + 32 * -x',
+            '-72x ^ 3 + 96x ^ 2 - 32x'])

+ 15 - 2
tests/test_parser.py

@@ -4,6 +4,8 @@ import unittest
 from src.parser import Parser
 from src.node import ExpressionNode as Node, ExpressionLeaf as Leaf
 from tests.parser import ParserWrapper, run_expressions, line, graph
+from tests.rulestestcase import tree
+from src.rules.goniometry import sin, cos
 
 
 class TestParser(unittest.TestCase):
@@ -15,11 +17,11 @@ class TestParser(unittest.TestCase):
         run_expressions(Parser, [('a', Leaf('a'))])
 
     def test_graph(self):
-        assert graph(Parser, '4a') == ("""
+        self.assertEqual(graph(Parser, '4a'), ("""
          *
         ╭┴╮
         4 a
-        """).replace('\n        ', '\n')[1:-1]
+        """).replace('\n        ', '\n')[1:-1])
 
     def test_line(self):
         self.assertEqual(line(Parser, '4-a'), '4 - a')
@@ -35,3 +37,14 @@ class TestParser(unittest.TestCase):
         self.assertNotEqual(possibilities2, [])
 
         self.assertNotEqual(possibilities1, possibilities2)
+
+    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)

+ 1 - 1
tests/test_rules_goniometry.py

@@ -6,7 +6,7 @@ from tests.rulestestcase import RulesTestCase, tree
 class TestRulesGoniometry(RulesTestCase):
 
     def test_match_add_quadrants(self):
-        root = tree('sin(x) ^ 2 + cos(x) ^ 2')
+        root = tree('sin x ^ 2 + cos x ^ 2')
         possibilities = match_add_quadrants(root)
         self.assertEqualPos(possibilities, [P(root, add_quadrants, ())])
 

+ 8 - 0
tests/test_rules_powers.py

@@ -47,6 +47,14 @@ class TestRulesPowers(RulesTestCase):
         self.assertEqualPos(possibilities,
                 [P(root, add_exponents, (Scope(root), n0, n1, a, p, q))])
 
+    def test_match_add_exponents_negated(self):
+        a, q = tree('a,q')
+        n0, n1 = root = (-a) * a ** q
+
+        possibilities = match_add_exponents(root)
+        self.assertEqualPos(possibilities,
+                [P(root, add_exponents, (Scope(root), n0, n1, a, 1, q))])
+
     def test_match_subtract_exponents_powers(self):
         a, p, q = tree('a,p,q')
         root = a ** p / a ** q

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