Jelajahi Sumber

Implemented and tested some new rules for fractions.

Taddeus Kroes 14 tahun lalu
induk
melakukan
c03390ba48

+ 5 - 5
src/rules/__init__.py

@@ -4,8 +4,8 @@ from .groups import match_combine_groups
 from .factors import match_expand
 from .powers import match_add_exponents, match_subtract_exponents, \
         match_multiply_exponents, match_duplicate_exponent, \
-        match_remove_negative_exponent, match_exponent_to_root, \
-        match_extend_exponent, match_constant_exponent
+        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
@@ -27,9 +27,9 @@ RULES = {
         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, match_constant_exponent,
-                 match_raise_numerics],
+                 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],

+ 23 - 4
src/rules/fractions.py

@@ -1,6 +1,6 @@
-from itertools import combinations
+from itertools import combinations, product
 
-from .utils import least_common_multiple
+from .utils import least_common_multiple, partition
 from ..node import ExpressionLeaf as L, Scope, negate, OP_DIV, OP_ADD, OP_MUL
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
@@ -178,17 +178,20 @@ def match_expand_and_add_fractions(node):
 def match_multiply_fractions(node):
     """
     a / b * (c / d)  ->  ac / (bd)
+    a * (b / c)      ->  ab / c
     """
-    # TODO: is 'add' Appropriate when rewriting to "(a + (-d)) / * (b / c)"?
     assert node.is_op(OP_MUL)
 
     p = []
     scope = Scope(node)
-    fractions = filter(lambda n: n.is_op(OP_DIV), scope)
+    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
 
 
@@ -207,3 +210,19 @@ def multiply_fractions(root, args):
 
 
 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}.')

+ 30 - 3
src/rules/powers.py

@@ -138,10 +138,10 @@ def match_duplicate_exponent(node):
     """
     assert node.is_op(OP_POW)
 
-    left, right = node
+    root, exponent = node
 
-    if left.is_op(OP_MUL):
-        return [P(node, duplicate_exponent, (list(Scope(left)), right))]
+    if root.is_op(OP_MUL):
+        return [P(node, duplicate_exponent, (list(Scope(root)), exponent))]
 
     return []
 
@@ -163,6 +163,33 @@ def duplicate_exponent(root, args):
 MESSAGES[duplicate_exponent] = _('Duplicate the exponent {2}.')
 
 
+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 []
+
+
+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):
     """
     a ^ -p  ->  1 / a ^ p

+ 17 - 0
src/rules/utils.py

@@ -54,3 +54,20 @@ def is_fraction(node, nominator, denominator):
                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

+ 11 - 6
tests/test_leiden_oefenopgave_v12.py

@@ -56,11 +56,16 @@ class TestLeidenOefenopgaveV12(TestCase):
             '(a2b^-1)^3(ab2)',
             '(a ^ 2 * (1 / b ^ 1)) ^ 3 * ab ^ 2',
             '(a ^ 2 * (1 / b)) ^ 3 * ab ^ 2',
-            '(a ^ 2) ^ 3 * (1 / b) ^ 3 * ab ^ 2',
-            'a ^ (2 * 3)(1 / b) ^ 3 * ab ^ 2',
-            'a ^ 6 * (1 / b) ^ 3 * ab ^ 2',
-            'a ^ (6 + 1)(1 / b) ^ 3 * b ^ 2',
-            'a ^ 7 * (1 / b) ^ 3 * b ^ 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',
+            # FIXME: 'b ^ 2 * a ^ 7 / b ^ 3',
+            # FIXME: 'b ^ (2 - 3) * a ^ 7',
         ])
 
     def test_2_b(self):
@@ -87,7 +92,7 @@ class TestLeidenOefenopgaveV12(TestCase):
         self.assertRewrite([
             '4b^-2',
             '4(1 / b ^ 2)',
-            # FIXME: '4 * 1/b ^ 2',
+            '4 * 1 / b ^ 2',
         ])
 
     def test_2_f(self):

+ 7 - 2
tests/test_rules_fractions.py

@@ -1,7 +1,7 @@
 from src.rules.fractions import match_constant_division, division_by_one, \
         division_of_zero, division_by_self, match_add_constant_fractions, \
         equalize_denominators, add_nominators, match_multiply_fractions, \
-        multiply_fractions
+        multiply_fractions, multiply_with_fraction
 from src.node import Scope
 from src.possibilities import Possibility as P
 from tests.rulestestcase import RulesTestCase, tree
@@ -127,10 +127,15 @@ class TestRulesFractions(RulesTestCase):
 
     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)),

+ 13 - 0
tests/test_rules_powers.py

@@ -2,6 +2,7 @@ from src.rules.powers import match_add_exponents, add_exponents, \
         match_subtract_exponents, subtract_exponents, \
         match_multiply_exponents, multiply_exponents, \
         match_duplicate_exponent, duplicate_exponent, \
+        match_raised_fraction, raised_fraction, \
         match_remove_negative_exponent, remove_negative_exponent, \
         match_exponent_to_root, exponent_to_root, \
         match_constant_exponent, remove_power_of_zero, remove_power_of_one
@@ -95,6 +96,18 @@ class TestRulesPowers(RulesTestCase):
         self.assertEqualPos(possibilities,
                 [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):
         a, p = tree('a,p')
         root = a ** -p

+ 5 - 1
tests/test_rules_utils.py

@@ -1,6 +1,6 @@
 import unittest
 
-from src.rules.utils import least_common_multiple, is_fraction
+from src.rules.utils import least_common_multiple, is_fraction, partition
 from tests.rulestestcase import tree
 
 
@@ -18,3 +18,7 @@ class TestRulesUtils(unittest.TestCase):
         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]))