Kaynağa Gözat

Commenced some chanches to fraction rules (unit tests are still failing).

Taddeus Kroes 14 yıl önce
ebeveyn
işleme
7a9605a745

+ 5 - 6
src/rules/__init__.py

@@ -9,9 +9,9 @@ from .powers import match_add_exponents, match_subtract_exponents, \
 from .numerics import match_add_numerics, match_divide_numerics, \
         match_multiply_numerics, match_multiply_zero, match_multiply_one, \
         match_raise_numerics
-from .fractions import match_constant_division, match_add_constant_fractions, \
-        match_expand_and_add_fractions, match_multiply_fractions, \
-        match_divide_fractions, match_equal_fraction_parts
+from .fractions import match_constant_division, match_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
@@ -30,12 +30,11 @@ from src.rules.integrals import match_solve_indef, match_constant_integral, \
 from src.rules.lineq import match_move_term
 
 RULES = {
-        OP_ADD: [match_add_numerics, match_add_constant_fractions,
+        OP_ADD: [match_add_numerics, match_add_fractions,
                  match_combine_groups, match_add_quadrants,
                  match_add_logarithms],
         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_multiply_zero, match_negated_factor, match_multiply_one,
                  match_sort_multiplicants, match_multiply_fractions,
                  match_factor_in_multiplicant],
         OP_DIV: [match_subtract_exponents, match_divide_numerics,

+ 204 - 157
src/rules/fractions.py

@@ -1,8 +1,9 @@
 from itertools import combinations, product
 
-from .utils import least_common_multiple, partition
-from ..node import ExpressionLeaf as L, Scope, OP_DIV, OP_ADD, OP_MUL, \
-        OP_POW, nary_node, negate
+from .utils import least_common_multiple, partition, is_numeric_node, \
+        evals_to_numeric
+from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, OP_DIV, \
+        OP_ADD, OP_MUL, OP_POW, nary_node, negate
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 
@@ -68,51 +69,76 @@ def division_by_self(root, args):
 MESSAGES[division_by_self] = _('Division of {1} by itself reduces to 1.')
 
 
-def match_add_constant_fractions(node):
+def match_add_fractions(node):
     """
-    1 / 2 + 3 / 4  ->  2 / 4 + 3 / 4  # Equalize denominators
-    2 / 2 - 3 / 4  ->  4 / 4 - 3 / 4
-    2 / 4 + 3 / 4  ->  5 / 4          # Equal denominators, so nominators can
-                                      # be added
-    2 / 4 - 3 / 4  ->  -1 / 4
-    1 / 2 + 3 / 4  ->  4 / 8 + 6 / 8  # Equalize denominators by multiplying
-                                      # them with eachother
-
+    a / b + c / b and a, c in Z        ->  (a + c) / b
+    a / b + c / d and a, b, c, d in Z  ->  a' / e + c' / e  # e = lcm(b, d)
+                                                            # | e = b * d
+    a / b + c and a, b, c in Z         ->  a / b + b / b * c # =>* (a + bc) / b
     """
     assert node.is_op(OP_ADD)
 
     p = []
     scope = Scope(node)
+    fractions, others = partition(lambda n: n.is_op(OP_DIV), scope)
+    numerics = filter(is_numeric_node, others)
 
-    fractions = filter(lambda node: node.is_op(OP_DIV), scope)
-
-    for a, b in combinations(fractions, 2):
-        na, da = a
-        nb, db = b
+    for ab, cd in combinations(fractions, 2):
+        a, b = ab
+        c, d = cd
 
-        if da == db:
+        if b == d:
             # Equal denominators, add nominators to create a single fraction
-            p.append(P(node, add_nominators, (a, b)))
-        elif da.is_numeric() and db.is_numeric():
+            p.append(P(node, add_nominators, (scope, ab, cd)))
+        elif all(map(is_numeric_node, (a, b, c, d))):
             # Denominators are both numeric, rewrite both fractions to the
             # least common multiple of their denominators. Later, the
             # nominators will be added
-            denom = least_common_multiple(da.value, db.value)
-            p.append(P(node, equalize_denominators, (scope, a, b, denom)))
+            lcm = least_common_multiple(b.value, d.value)
+            p.append(P(node, equalize_denominators, (scope, ab, cd, lcm)))
 
             # Also, add the (non-recommended) possibility to multiply the
-            # denominators
-            p.append(P(node, equalize_denominators, (scope, a, b,
-                                                     da.value * db.value)))
+            # denominators. Do this only if the multiplication is not equal to
+            # the least common multiple, to avoid duplicate possibilities
+            mult = b.value * d.value
+
+            if mult != lcm:
+                p.append(P(node, equalize_denominators, (scope, ab, cd, mult)))
+
+    for ab, c in product(fractions, numerics):
+        a, b = ab
+
+        if a.is_numeric() and b.is_numeric():
+            # Fraction of constants added to a constant -> create a single
+            # constant fraction
+            p.append(P(node, constant_to_fraction, (scope, ab, c)))
 
     return p
 
 
+def add_nominators(root, args):
+    """
+    a / b + c / b and a, c in Z  ->  (a + c) / b
+    """
+    scope, ab, cb = args
+    a, b = ab
+    c = cb[0]
+
+    # Replace the left node with the new expression, transfer fraction
+    # negations to nominators
+    scope.replace(ab, (a.negate(ab.negated) + c.negate(cb.negated)) / b)
+    scope.remove(cb)
+
+    return scope.as_nary_node()
+
+
+MESSAGES[add_nominators] = \
+        _('Add the nominators of {2} and {3} to create a single fraction.')
+
+
 def equalize_denominators(root, args):
     """
-    1 / 2 + 3 / 4  ->  2 / 4 + 3 / 4
-    1 / 2 - 3 / 4  ->  2 / 4 - 3 / 4
-    a / 2 + b / 4  ->  2a / 4 + b / 4
+    a / b + c / d and a, b, c, d in Z  ->  a' / e + c' / e
     """
     scope, denom = args[::3]
 
@@ -132,65 +158,46 @@ def equalize_denominators(root, args):
     return scope.as_nary_node()
 
 
-MESSAGES[equalize_denominators] = _('Equalize the denominators of divisions'
-    ' {2} and {3} to {4}.')
+MESSAGES[equalize_denominators] = \
+        _('Equalize the denominators of divisions' ' {2} and {3} to {4}.')
 
 
-def add_nominators(root, args):
+def constant_to_fraction(root, args):
     """
-    a / b + c / b      ->  (a + c) / b
-    a / b - c / b      ->  (a - c) / b
-    -(a / b) + c / b   ->  -((a + c) / b)
-    -(a / b) - c / b   ->  (c - a) / -b
+    a / b + c and a, b, c in Z  ->  a / b + b / b * c  # =>* (a + bc) / b
     """
-    # TODO: is 'add' Appropriate when rewriting to "(a + (-c)) / b"?
-    ab, cb = args
-    a, b = ab
-    scope = Scope(root)
-
-    # Replace the left node with the new expression
-    scope.replace(ab, (a + cb[0].negate(cb.negated)) / b)
-
-    # Remove the right node
-    scope.remove(cb)
+    scope, ab, c = args
+    b = ab[1]
+    scope.replace(c, b / b * c)
 
     return scope.as_nary_node()
 
 
-# TODO: convert this to a lambda. Example: 22 / 77 - 28 / 77. the "-" is above
-# the "28/77" division.
-MESSAGES[add_nominators] = _('Add the nominators of {1} and {2}.')
-
-
-def match_expand_and_add_fractions(node):
-    """
-    a * b / c + d * b / c      ->  (a + d) * (b / c)
-    a * b / c + (- d * b / c)  ->  (a + (-d)) * (b / c)
-    """
-    # TODO: is 'add' Appropriate when rewriting to "(a + (-d)) / * (b / c)"?
-    assert node.is_op(OP_MUL)
-
-    p = []
-
-    return p
+MESSAGES[constant_to_fraction] = \
+        _('Rewrite constant {3} to a fraction to be able to add it to {2}.')
 
 
 def match_multiply_fractions(node):
     """
     a / b * (c / d)  ->  ac / (bd)
-    a * (b / c)      ->  ab / c
+    a / b * c and a, c in Z or (a = 1 and eval(b) not in Z)  ->  ac / b
     """
     assert node.is_op(OP_MUL)
 
     p = []
     scope = Scope(node)
     fractions, others = partition(lambda n: n.is_op(OP_DIV), scope)
+    numerics = filter(is_numeric_node, others)
 
     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)))
+    for ab, c in product(fractions, others):
+        a, b = ab
+
+        if (a.is_numeric() and c.is_numeric()) or \
+                (a == 1 and evals_to_numeric(b)):
+            p.append(P(node, multiply_with_fraction, (scope, ab, c)))
 
     return p
 
@@ -203,7 +210,7 @@ def multiply_fractions(root, args):
     a, b = ab
     c, d = cd
 
-    scope.replace(ab, a * c / (b * d))
+    scope.replace(ab, (a * c / (b * d)).negate(ab.negated + cd.negated))
     scope.remove(cd)
 
     return scope.as_nary_node()
@@ -214,18 +221,19 @@ MESSAGES[multiply_fractions] = _('Multiply fractions {2} and {3}.')
 
 def multiply_with_fraction(root, args):
     """
-    a * (b / c)  ->  ab / c
+    a / b * c and a, c in Z or a == 1  ->  ac / b
     """
-    scope, a, bc = args
-    b, c = bc
+    scope, ab, c = args
+    a, b = ab
 
-    scope.replace(a, a * b / c)
-    scope.remove(bc)
+    scope.replace(ab, (a * c / b).negate(ab.negated))
+    scope.remove(c)
 
     return scope.as_nary_node()
 
 
-MESSAGES[multiply_with_fraction] = _('Multiply {2} with fraction {3}.')
+MESSAGES[multiply_with_fraction] = \
+        _('Multiply {3} with the nominator of fraction {2}.')
 
 
 def match_divide_fractions(node):
@@ -306,32 +314,48 @@ def fraction_scopes(node):
     nominator, denominator = node
 
     if nominator.is_op(OP_MUL):
-        n_scope = list(Scope(nominator))
+        n_scope = Scope(nominator)
     else:
-        n_scope = [nominator]
+        n_scope = Scope(N(OP_MUL, nominator))
 
     if denominator.is_op(OP_MUL):
-        d_scope = list(Scope(denominator))
+        d_scope = Scope(denominator)
     else:
-        d_scope = [denominator]
+        d_scope = Scope(N(OP_MUL, denominator))
 
     return n_scope, d_scope
 
 
+def is_power_combination(a, b):
+    """
+    Check if two nodes are powers that can be combined in a fraction, for
+    example:
+
+    a and a^2
+    a^2 and a^2
+    a^2 and a
+    """
+    if a.is_power():
+        a = a[0]
+
+    if b.is_power():
+        b = b[0]
+
+    return a == b
+
+
 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
+    a ^ b * c / (a ^ d * e)  ->  a ^ b / a ^ d * (c / e)
 
-    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
+    #If the same root appears in both nominator and denominator, extract 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)
 
@@ -343,88 +367,111 @@ def match_equal_fraction_parts(node):
         return 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)))
+    for n, d in product(n_scope, d_scope):
+        if is_power_combination(n, d):
+            p.append(P(N, extract_fraction_terms, (n_scope, d_scope, n, d)))
+
+    #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):
+def extract_fraction_terms(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 ^ b * c / (a ^ d * e)  ->  a ^ b / a ^ d * (c / e)
     """
-    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}.')
+    n_scope, d_scope, n, d = args
 
+    if len(n_scope) == 1:
+        n_scope.replace(n, L(1))
+    else:
+        n_scope.remove(n)
 
-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)
+    if len(d_scope) == 1:
+        d_scope.replace(d, L(1))
+    else:
+        d_scope.remove(n)
 
-    return ap / aq * nom.negate(n.negated) / denom.negate(d.negated)
+    return n / d * (n_scope.as_nary_node() / d_scope.as_nary_node())
 
 
-MESSAGES[extract_divided_roots] = \
-        _('Extract the root {1} from nominator and denominator in {0}.')
+#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}.')

+ 22 - 1
src/rules/utils.py

@@ -1,4 +1,5 @@
-from ..node import ExpressionNode as N, ExpressionLeaf as L, OP_MUL, OP_DIV
+from ..node import ExpressionNode as N, ExpressionLeaf as L, OP_MUL, OP_DIV, \
+        OP_ADD, OP_POW, OP_SQRT
 
 
 def greatest_common_divisor(a, b):
@@ -132,3 +133,23 @@ def divides(m, n):
     Check if m | n (m divides n).
     """
     return not divmod(n, m)[1]
+
+
+def is_numeric_node(node):
+    """
+    Check if a node is numeric.
+    """
+    return node.is_numeric()
+
+
+def evals_to_numeric(node):
+    """
+    Check if a node will eventually evaluate to a numeric value, by checking if
+    all leaves are numeric and there are only operators that can be
+    considerered a constant or will evaluate to one (+, *, /, ^, sqrt).
+    """
+    if node.is_leaf:
+        return node.is_numeric()
+
+    return node.op in (OP_ADD, OP_MUL, OP_DIV, OP_POW, OP_SQRT) \
+           and all(map(evals_to_numeric, node))

+ 139 - 82
tests/test_rules_fractions.py

@@ -1,9 +1,9 @@
 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_fractions, \
         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
+        constant_to_fraction, extract_fraction_terms
 from src.node import Scope
 from src.possibilities import Possibility as P
 from tests.rulestestcase import RulesTestCase, tree
@@ -49,44 +49,51 @@ class TestRulesFractions(RulesTestCase):
 
         self.assertEqualNodes(division_by_self(root, ()), one)
 
-    def test_match_add_constant_fractions(self):
+    def test_match_add_fractions(self):
         a, b, c, l1, l2, l3, l4 = tree('a,b,c,1,2,3,4')
 
         n0, n1 = root = l1 / l2 + l3 / l4
-        possibilities = match_add_constant_fractions(root)
+        possibilities = match_add_fractions(root)
         self.assertEqualPos(possibilities,
                 [P(root, equalize_denominators, (Scope(root), n0, n1, 4)),
                  P(root, equalize_denominators, (Scope(root), n0, n1, 8))])
 
         (((n0, n1), n2), n3), n4 = root = a + l1 / l2 + b + l3 / l4 + c
-        possibilities = match_add_constant_fractions(root)
+        possibilities = match_add_fractions(root)
         self.assertEqualPos(possibilities,
                 [P(root, equalize_denominators, (Scope(root), n1, n3, 4)),
                  P(root, equalize_denominators, (Scope(root), n1, n3, 8))])
 
         n0, n1 = root = l2 / l4 + l3 / l4
-        possibilities = match_add_constant_fractions(root)
+        possibilities = match_add_fractions(root)
         self.assertEqualPos(possibilities,
-                [P(root, add_nominators, (n0, n1))])
+                [P(root, add_nominators, (Scope(root), n0, n1))])
 
         (((n0, n1), n2), n3), n4 = root = a + l2 / l4 + b + l3 / l4 + c
-        possibilities = match_add_constant_fractions(root)
+        possibilities = match_add_fractions(root)
         self.assertEqualPos(possibilities,
-                [P(root, add_nominators, (n1, n3))])
+                [P(root, add_nominators, (Scope(root), n1, n3))])
 
-    def test_add_constant_fractions_with_negation(self):
+    def test_match_add_fractions_constant_to_fraction(self):
+        l23, l1 = root = tree('2 / 3 + 1')
+        self.assertEqualPos(match_add_fractions(root),
+                [P(root, constant_to_fraction, (Scope(root), l23, l1))])
+
+    def test_add_fractions_with_negation(self):
         a, b, c, l1, l2, l3, l4 = tree('a,b,c,1,2,3,4')
 
         (((n0, n1), n2), n3), n4 = root = a + l2 / l2 + b + (-l3 / l4) + c
-        possibilities = match_add_constant_fractions(root)
-        self.assertEqualPos(possibilities,
+        self.assertEqualPos(match_add_fractions(root),
                 [P(root, equalize_denominators, (Scope(root), n1, n3, 4)),
                  P(root, equalize_denominators, (Scope(root), n1, n3, 8))])
 
+        n0, n1 = root = l1 / l2 + l4 / l3
+        self.assertEqualPos(match_add_fractions(root),
+                [P(root, equalize_denominators, (Scope(root), n0, n1, 6))])
+
         (((n0, n1), n2), n3), n4 = root = a + l2 / l4 + b + (-l3 / l4) + c
-        possibilities = match_add_constant_fractions(root)
-        self.assertEqualPos(possibilities,
-                [P(root, add_nominators, (n1, n3))])
+        self.assertEqualPos(match_add_fractions(root),
+                [P(root, add_nominators, (Scope(root), n1, n3))])
 
     def test_equalize_denominators(self):
         a, b, l1, l2, l3, l4 = tree('a,b,1,2,3,4')
@@ -113,30 +120,44 @@ class TestRulesFractions(RulesTestCase):
     def test_add_nominators(self):
         a, b, c = tree('a,b,c')
         n0, n1 = root = a / b + c / b
-        self.assertEqualNodes(add_nominators(root, (n0, n1)), (a + c) / b)
+        self.assertEqualNodes(add_nominators(root, (Scope(root), n0, n1)),
+                              (a + c) / b)
 
         n0, n1 = root = a / b + -c / b
-        self.assertEqualNodes(add_nominators(root, (n0, n1)), (a + -c) / b)
+        self.assertEqualNodes(add_nominators(root, (Scope(root), n0, n1)),
+                              (a + -c) / b)
 
         n0, n1 = root = a / b + -(c / b)
-        self.assertEqualNodes(add_nominators(root, (n0, n1)), (a + -c) / b)
+        self.assertEqualNodes(add_nominators(root, (Scope(root), n0, n1)),
+                              (a + -c) / b)
 
         n0, n1 = root = a / -b + c / -b
-        self.assertEqualNodes(add_nominators(root, (n0, n1)), (a + c) / -b)
+        self.assertEqualNodes(add_nominators(root, (Scope(root), n0, n1)),
+                              (a + c) / -b)
 
         n0, n1 = root = a / -b + -c / -b
-        self.assertEqualNodes(add_nominators(root, (n0, n1)), (a + -c) / -b)
+        self.assertEqualNodes(add_nominators(root, (Scope(root), n0, n1)),
+                              (a + -c) / -b)
+
+    def test_constant_to_fraction(self):
+        root, e = tree('2 / 3 + 1, 2 / 3 + 3 / 3 * 1')
+        l23, l1 = root
+        self.assertEqual(constant_to_fraction(root, (Scope(root), l23, l1)), e)
 
     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)')
+        (ab, e), cd = root = tree('4 / b * 2 * (3 / 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))])
+                 P(root, multiply_with_fraction, (Scope(root), ab, e)),
+                 P(root, multiply_with_fraction, (Scope(root), cd, e))])
+
+        ab, c = root = tree('1 / sqrt(3) * 2')
+        self.assertEqualPos(match_multiply_fractions(root),
+                [P(root, multiply_with_fraction, (Scope(root), ab, c))])
 
     def test_multiply_fractions(self):
         (a, b), (c, d) = ab, cd = root = tree('a / b * (c / d)')
@@ -165,78 +186,114 @@ class TestRulesFractions(RulesTestCase):
         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)')
+        root, a, b, c = tree('ab / (ca), a, b, c')
+        n, d = root
         self.assertEqualPos(match_equal_fraction_parts(root),
-                [P(root, divide_fraction_parts, (a, [a, b], [c, a], 0, 1))])
+                [P(root, extract_fraction_terms, (Scope(n), Scope(d), a, a))])
 
-        (a, b), a = root = tree('ab / a')
+        n, d = root = tree('ab / a')
         self.assertEqualPos(match_equal_fraction_parts(root),
-                [P(root, divide_fraction_parts, (a, [a, b], [a], 0, 0))])
+                [P(root, extract_fraction_terms, (Scope(n), Scope(d), a, a))])
 
-        a, (a, b) = root = tree('a / (ab)')
+        n, d = root = tree('a / (ab)')
         self.assertEqualPos(match_equal_fraction_parts(root),
-                [P(root, divide_fraction_parts, (a, [a], [a, b], 0, 0))])
+                [P(root, extract_fraction_terms, (Scope(n), Scope(d), a, a))])
 
-        root = tree('abc / (cba)')
-        ((a, b), c) = root[0]
-        s0, s1 = [a, b, c], [c, b, a]
+        n, d = root = tree('abc / (cba)')
         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))])
+                [P(root, extract_fraction_terms, (Scope(n), scope(d), a, a)),
+                 P(root, extract_fraction_terms, (Scope(n), scope(d), b, b)),
+                 P(root, extract_fraction_terms, (Scope(n), scope(d), c, c))])
 
         root = tree('a / a')
         self.assertEqualPos(match_equal_fraction_parts(root), [])
 
         (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))])
+        self.assertequalpos(match_equal_fraction_parts(root),
+                [p(root, extract_fraction_terms, (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))])
+        self.assertequalpos(match_equal_fraction_parts(root),
+                [p(root, extract_fraction_terms, (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('(-a)b / 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)
+        self.assertequalpos(match_equal_fraction_parts(root),
+                [p(root, extract_fraction_terms, (a, [ap, b], [a], 0, 0))])
+
+    #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)

+ 11 - 1
tests/test_rules_utils.py

@@ -1,7 +1,7 @@
 from src.rules import utils
 from src.rules.utils import least_common_multiple, is_fraction, partition, \
         find_variables, first_sorted_variable, find_variable, substitute, \
-        divides
+        divides, evals_to_numeric
 from tests.rulestestcase import tree, RulesTestCase
 
 
@@ -65,3 +65,13 @@ class TestRulesUtils(RulesTestCase):
         self.assertTrue(divides(7, 21))
         self.assertFalse(divides(4, 2))
         self.assertFalse(divides(2, 3))
+
+    def test_evals_to_numeric(self):
+        self.assertTrue(evals_to_numeric(tree('1')))
+        self.assertFalse(evals_to_numeric(tree('a')))
+        self.assertTrue(evals_to_numeric(tree('1 + 2')))
+        self.assertFalse(evals_to_numeric(tree('1 + a')))
+        self.assertTrue(evals_to_numeric(tree('1 + 2 / 2 * 9')))
+        self.assertFalse(evals_to_numeric(tree('int 1')))
+        self.assertFalse(evals_to_numeric(tree('int a')))
+        self.assertTrue(evals_to_numeric(tree('sqrt 1')))