Jelajahi Sumber

Fine-tuned fraction rules and updated unit tests accordingly.

Taddeus Kroes 14 tahun lalu
induk
melakukan
c9d72b477e

+ 52 - 46
src/rules/fractions.py

@@ -1,4 +1,5 @@
 from itertools import combinations, product
+import copy
 
 from .utils import least_common_multiple, partition, is_numeric_node, \
         evals_to_numeric
@@ -288,28 +289,6 @@ MESSAGES[divide_by_fraction] = \
         _('Move {3} to nominator of fraction {1} / {2}.')
 
 
-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 = Scope(nominator)
-    else:
-        n_scope = Scope(N(OP_MUL, nominator))
-
-    if denominator.is_op(OP_MUL):
-        d_scope = Scope(denominator)
-    else:
-        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
@@ -328,37 +307,73 @@ def is_power_combination(a, b):
     return a == b
 
 
+def mult_scope(node):
+    """
+    Get the multiplication scope of a node that may or may no be a
+    multiplication itself.
+    """
+    if node.is_op(OP_MUL):
+        return Scope(node)
+
+    return Scope(N(OP_MUL, node))
+
+
+def remove_from_mult_scope(scope, node):
+    if len(scope) == 1:
+        scope.replace(node, L(1))
+    else:
+        scope.remove(node)
+
+    return scope.as_nary_node()
+
+
 def match_extract_fraction_terms(node):
     """
-    Divide nominator and denominator by the same part.
+    Divide nominator and denominator by the same part. If the same root of a
+    power appears in both nominator and denominator, also extract it so that it
+    can be reduced to a single power by power division rules.
 
     Examples:
-    a ^ b * c / (a ^ d * e)  ->  a ^ b / a ^ d * (c / e)
+    ab / (ac)                ->  a / a * (c / e)          # =>* c / e
+    a ^ b * c / (a ^ d * e)  ->  a ^ b / a ^ d * (c / e)  # -> a^(b - d)(c / e)
 
-    #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
+    ac / b and eval(c) not in Z and eval(a / b) in Z  ->  a / b * c
     """
-    # TODO: ac / b  ->  a / b * c
     assert node.is_op(OP_DIV)
 
-    nominator, denominator = node
-    n_scope, d_scope = fraction_scopes(node)
+    n_scope, d_scope = map(mult_scope, node)
     p = []
 
     if len(n_scope) == 1 and len(d_scope) == 1:
         return p
 
-    # Look for matching parts in scopes
-    for n, d in product(n_scope, d_scope):
-        if is_power_combination(n, d):
+    nominator, denominator = node
+
+    for n in n_scope:
+        # ac / b
+        if not evals_to_numeric(n):
+            a_scope = mult_scope(nominator)
+            a = remove_from_mult_scope(a_scope, n)
+
+            if evals_to_numeric(a / denominator):
+                p.append(P(node, extract_nominator_term, (a, n)))
+
+        # a ^ b * c / (a ^ d * e)
+        for d in [d for d in d_scope if is_power_combination(n, d)]:
             p.append(P(node, extract_fraction_terms, (n_scope, d_scope, n, d)))
 
     return p
 
 
+def extract_nominator_term(root, args):
+    """
+    ac / b and eval(c) not in Z and eval(a / b) in Z  ->  a / b * c
+    """
+    a, c = args
+
+    return a / root[1] * c
+
+
 def extract_fraction_terms(root, args):
     """
     ab / a                   ->  a / a * (b / 1)
@@ -368,17 +383,8 @@ def extract_fraction_terms(root, args):
     """
     n_scope, d_scope, n, d = args
 
-    if len(n_scope) == 1:
-        n_scope.replace(n, L(1))
-    else:
-        n_scope.remove(n)
-
-    if len(d_scope) == 1:
-        d_scope.replace(d, L(1))
-    else:
-        d_scope.remove(d)
-
-    return n / d * (n_scope.as_nary_node() / d_scope.as_nary_node())
+    return n / d * (remove_from_mult_scope(n_scope, n) \
+                    / remove_from_mult_scope(d_scope, d))
 
 
 MESSAGES[extract_fraction_terms] = _('Extract {3} / {4} from fraction {0}.')

+ 13 - 12
tests/test_leiden_oefenopgave_v12.py

@@ -63,22 +63,22 @@ class TestLeidenOefenopgaveV12(TestCase):
             '(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',
+            '(1a ^ 2 / 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 ^ 6 * a / b ^ 3 * b ^ 2',
+            'a ^ (6 + 1) / 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 ^ 2 / 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)',
+            '1 / b * a ^ 7',
+            '1a ^ 7 / b',
             'a ^ 7 / b',
         ])
 
@@ -106,7 +106,8 @@ class TestLeidenOefenopgaveV12(TestCase):
         self.assertRewrite([
             '4b^-2',
             '4(1 / b ^ 2)',
-            '4 * 1 / b ^ 2',
+            '1 * 4 / b ^ 2',
+            '4 / b ^ 2',
         ])
 
     def test_2_f(self):

+ 1 - 1
tests/test_rules_derivatives.py

@@ -113,7 +113,7 @@ class TestRulesDerivatives(RulesTestCase):
             "e ^ (xln(x))(ln(x) + x(1 / (xln(e))))",
             "e ^ (xln(x))(ln(x) + x(1 / (x * 1)))",
             "e ^ (xln(x))(ln(x) + x(1 / x))",
-            "e ^ (xln(x))(ln(x) + x * 1 / x)",
+            "e ^ (xln(x))(ln(x) + 1x / x)",
             "e ^ (xln(x))(ln(x) + x / x)",
             "e ^ (xln(x))(ln(x) + 1)",
             "e ^ ln(x ^ x)(ln(x) + 1)",

+ 18 - 1
tests/test_rules_fractions.py

@@ -3,7 +3,7 @@ from src.rules.fractions import match_constant_division, division_by_one, \
         equalize_denominators, add_nominators, match_multiply_fractions, \
         multiply_fractions, multiply_with_fraction, match_divide_fractions, \
         divide_fraction, divide_by_fraction, match_extract_fraction_terms, \
-        constant_to_fraction, extract_fraction_terms
+        constant_to_fraction, extract_nominator_term, extract_fraction_terms
 from src.node import ExpressionNode as N, Scope, OP_MUL
 from src.possibilities import Possibility as P
 from tests.rulestestcase import RulesTestCase, tree
@@ -234,6 +234,23 @@ class TestRulesFractions(RulesTestCase):
         self.assertEqualPos(match_extract_fraction_terms(root),
                 [P(root, extract_fraction_terms, (Scope(n), lscp(d), ap, a))])
 
+        (l2, a), l3 = n, d = root = tree('2a / 3')
+        self.assertEqualPos(match_extract_fraction_terms(root),
+                [P(root, extract_nominator_term, (2, a))])
+
+        root = tree('2*4 / 3')
+        self.assertEqualPos(match_extract_fraction_terms(root), [])
+
+        n, d = root = tree('2a / 2')
+        self.assertEqualPos(match_extract_fraction_terms(root),
+                [P(root, extract_fraction_terms, (Scope(n), lscp(d), 2, 2)),
+                 P(root, extract_nominator_term, (2, a))])
+
+    def test_extract_nominator_term(self):
+        root, expect = tree('2a / 3, 2 / 3 * a')
+        l2, a = root[0]
+        self.assertEqual(extract_nominator_term(root, (l2, a)), expect)
+
     def test_extract_fraction_terms_basic(self):
         root, expect = tree('ab / (ca), a / a * (b / c)')
         n, d = root

+ 3 - 1
tests/test_rules_lineq.py

@@ -75,7 +75,9 @@ class TestRulesLineq(RulesTestCase):
             '5x = 0 - 5',
             '5x = -5',
             '5x / 5 = (-5) / 5',
-            'x / 1 = (-5) / 5',
+            '5 / 5 * (x / 1) = (-5) / 5',
+            '1(x / 1) = (-5) / 5',
+            '1x = (-5) / 5',
             'x = (-5) / 5',
             'x = -5 / 5',
             'x = -1',