Преглед изворни кода

Merge branch 'master' into gonio

Conflicts:
	tests/test_rules_goniometry.py
Taddeus Kroes пре 14 година
родитељ
комит
6139bd8225

+ 8 - 39
TODO

@@ -2,47 +2,9 @@
 
 
  - 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 +46,10 @@ 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)

+ 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', ('COMMA', )),
         ('left', ('MINUS', 'PLUS')),
         ('left', ('MINUS', 'PLUS')),
         ('left', ('TIMES', 'DIVIDE')),
         ('left', ('TIMES', 'DIVIDE')),
+        ('left', ('EQ', )),
         ('left', ('NEG', )),
         ('left', ('NEG', )),
         ('right', ('POW', )),
         ('right', ('POW', )),
+        ('right', ('SIN', 'COS', 'TAN', 'SOLVE', 'INT', 'SQRT')),
         )
         )
 
 
     interactive = 0
     interactive = 0
@@ -154,7 +156,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).upper() in self.tokens:
                 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
@@ -334,6 +336,12 @@ 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
+              | SIN exp
+              | COS exp
+              | TAN exp
+              | INT exp
+              | SOLVE exp
+              | SQRT exp
         """
         """
 
 
         if option == 0:  # rule: NEG exp
         if option == 0:  # rule: NEG exp
@@ -346,6 +354,11 @@ class Parser(BisonParser):
 
 
             return values[1]
             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".'
         raise BisonSyntaxError('Unsupported option %d in target "%s".'
                                % (option, target))  # pragma: nocover
                                % (option, target))  # pragma: nocover
 
 
@@ -355,13 +368,14 @@ 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
+        if option == 5:  # rule: exp MINUS exp
             node = values[2]
             node = values[2]
 
 
             # Add negation to the left-most child
             # Add negation to the left-most child
@@ -436,7 +450,6 @@ class Parser(BisonParser):
     [a-zA-Z]  { returntoken(IDENTIFIER); }
     [a-zA-Z]  { returntoken(IDENTIFIER); }
     "("       { returntoken(LPAREN); }
     "("       { returntoken(LPAREN); }
     ")"       { returntoken(RPAREN); }
     ")"       { returntoken(RPAREN); }
-    ","       { returntoken(COMMA); }
     """ + operators + r"""
     """ + operators + r"""
     "raise"   { returntoken(RAISE); }
     "raise"   { returntoken(RAISE); }
     "graph"   { returntoken(GRAPH); }
     "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
         match_expand_and_add_fractions
 from .negation import match_negated_factor, match_negate_polynome, \
 from .negation import match_negated_factor, match_negate_polynome, \
         match_negated_division
         match_negated_division
+from .sort import match_sort_multiplicants
 
 
 RULES = {
 RULES = {
         OP_ADD: [match_add_numerics, match_add_constant_fractions,
         OP_ADD: [match_add_numerics, match_add_constant_fractions,
                  match_combine_groups],
                  match_combine_groups],
         OP_MUL: [match_multiply_numerics, match_expand, match_add_exponents,
         OP_MUL: [match_multiply_numerics, match_expand, match_add_exponents,
                  match_expand_and_add_fractions, match_multiply_zero,
                  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,
         OP_DIV: [match_subtract_exponents, match_divide_numerics,
                  match_constant_division, match_negated_division],
                  match_constant_division, match_negated_division],
         OP_POW: [match_multiply_exponents, match_duplicate_exponent,
         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 ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 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):
 def match_add_quadrants(node):
     """
     """
     sin(x) ^ 2 + cos(x) ^ 2  ->  1
     sin(x) ^ 2 + cos(x) ^ 2  ->  1

+ 2 - 2
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, 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 ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 from ..translate import _
 
 

+ 0 - 5
src/rules/numerics.py

@@ -38,11 +38,6 @@ def add_numerics(root, args):
     scope, c0, c1 = args
     scope, c0, c1 = args
     value = c0.actual_value() + c1.actual_value()
     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
     # Replace the left node with the new expression
     scope.replace(c0, Leaf(abs(value)).negate(int(value < 0)))
     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 itertools import combinations
 
 
 from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, \
 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 ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 from ..translate import _
 
 
@@ -12,6 +12,7 @@ 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)
 
 
@@ -20,12 +21,12 @@ def match_add_exponents(node):
     scope = Scope(node)
     scope = Scope(node)
 
 
     for n in scope:
     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:  # pragma: nocover
         else:  # pragma: nocover
             continue
             continue
@@ -53,8 +54,10 @@ def add_exponents(root, args):
     """
     """
     scope, n0, n1, a, p, q = args
     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.replace(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)

+ 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):
 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,11 +15,11 @@ 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)
@@ -36,20 +36,21 @@ class TestLeidenOefenopgave(TestCase):
                 '(x ^ 2 + 2x + 1 * 1)(x + 1)',
                 '(x ^ 2 + 2x + 1 * 1)(x + 1)',
                 '(x ^ 2 + 2x + 1)(x + 1)',
                 '(x ^ 2 + 2x + 1)(x + 1)',
                 '(x ^ 2 + 2x)x + (x ^ 2 + 2x) * 1 + 1x + 1 * 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 ^ (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 * 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)
             self.assertRewrite(chain)
@@ -115,7 +116,7 @@ class TestLeidenOefenopgave(TestCase):
 
 
     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)',
@@ -124,15 +125,15 @@ 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',
+                            '-25 + 16x ^ 2'])
+                            # FIXME: '16x ^ 2 - 25'])
 
 
     def test_2(self):
     def test_2(self):
         pass
         pass

+ 23 - 21
tests/test_leiden_oefenopgave_v12.py

@@ -4,7 +4,7 @@ from tests.rulestestcase import RulesTestCase as TestCase, rewrite
 class TestLeidenOefenopgaveV12(TestCase):
 class TestLeidenOefenopgaveV12(TestCase):
     def test_1_e(self):
     def test_1_e(self):
         self.assertRewrite([
         self.assertRewrite([
-            '-2(6x - 4) ^ 2 * x',
+            '-2(6x - 4) ^ 2x',
             '-2(6x - 4)(6x - 4)x',
             '-2(6x - 4)(6x - 4)x',
             '(-2 * 6x - 2 * -4)(6x - 4)x',
             '(-2 * 6x - 2 * -4)(6x - 4)x',
             '(-12x - 2 * -4)(6x - 4)x',
             '(-12x - 2 * -4)(6x - 4)x',
@@ -12,23 +12,25 @@ class TestLeidenOefenopgaveV12(TestCase):
             '(-12x + 8)(6x - 4)x',
             '(-12x + 8)(6x - 4)x',
             '(-12x * 6x - 12x * -4 + 8 * 6x + 8 * -4)x',
             '(-12x * 6x - 12x * -4 + 8 * 6x + 8 * -4)x',
             '(-72xx - 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.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,11 @@ 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')
@@ -35,3 +37,14 @@ class TestParser(unittest.TestCase):
         self.assertNotEqual(possibilities2, [])
         self.assertNotEqual(possibilities2, [])
 
 
         self.assertNotEqual(possibilities1, 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):
 class TestRulesGoniometry(RulesTestCase):
 
 
     def test_match_add_quadrants(self):
     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)
         possibilities = match_add_quadrants(root)
         self.assertEqualPos(possibilities, [P(root, add_quadrants, ())])
         self.assertEqualPos(possibilities, [P(root, add_quadrants, ())])
 
 

+ 8 - 0
tests/test_rules_powers.py

@@ -47,6 +47,14 @@ class TestRulesPowers(RulesTestCase):
         self.assertEqualPos(possibilities,
         self.assertEqualPos(possibilities,
                 [P(root, add_exponents, (Scope(root), n0, n1, a, p, q))])
                 [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')
         root = a ** p / a ** 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)