Bläddra i källkod

Merged conflicts.

Taddeus Kroes 14 år sedan
förälder
incheckning
1c68fcb3c6

+ 1 - 1
external/graph_drawing

@@ -1 +1 @@
-Subproject commit 21f0710c80184b6ea18827808ec40af924f0c8a8
+Subproject commit 79a5b196645611ab8b610685a8b1cf7ceef362af

+ 15 - 0
src/node.py

@@ -9,6 +9,8 @@ from graph_drawing.graph import generate_graph
 from graph_drawing.line import generate_line
 from graph_drawing.node import Node, Leaf
 
+from unicode_math import PI as u_PI
+
 
 TYPE_OPERATOR = 1
 TYPE_IDENTIFIER = 2
@@ -45,6 +47,9 @@ OP_HINT = 17
 OP_REWRITE_ALL = 18
 OP_REWRITE = 19
 
+# Special identifierd
+PI = 'pi'
+
 
 TYPE_MAP = {
         int: TYPE_INTEGER,
@@ -352,7 +357,17 @@ class ExpressionLeaf(Leaf, ExpressionBase):
         return self.negated == other.negated and self.type == other.type \
                and self.value == other.value
 
+    def __str__(self):
+        val = str(self.value)
+
+        # Replace PI leaf by the Greek character
+        if val == PI:
+            val = u_PI
+
+        return '-' * self.negated + val
+
     def __repr__(self):
+        return str(self)
         return '-' * self.negated + str(self.value)
 
     def equals(self, other, ignore_negation=False):

+ 5 - 4
src/parser.py

@@ -15,7 +15,7 @@ from pybison import BisonParser, BisonSyntaxError
 from graph_drawing.graph import generate_graph
 
 from node import ExpressionNode as Node, ExpressionLeaf as Leaf, OP_MAP, \
-        TOKEN_MAP, TYPE_OPERATOR, OP_COMMA, OP_NEG, OP_MUL, Scope
+        TOKEN_MAP, TYPE_OPERATOR, OP_COMMA, OP_NEG, OP_MUL, Scope, PI
 from rules import RULES
 from possibilities import filter_duplicates, pick_suggestion, apply_suggestion
 
@@ -45,7 +45,7 @@ class Parser(BisonParser):
 
     # Words to be ignored by preprocessor
     words = zip(*filter(lambda (s, op): TOKEN_MAP[op] == 'FUNCTION', \
-                        OP_MAP.iteritems()))[0] + ('raise', 'graph')
+                        OP_MAP.iteritems()))[0] + ('raise', 'graph', PI)
 
     # Output directory of generated pybison files, including a trailing slash.
     buildDirectory = PYBISON_BUILD + '/'
@@ -418,9 +418,10 @@ class Parser(BisonParser):
                                % (option, target))  # pragma: nocover
 
     # -----------------------------------------
-    # operator tokens
+    # PI and operator tokens
     # -----------------------------------------
-    operators = ''
+    operators = '"%s"%s{ returntoken(IDENTIFIER); }\n' \
+                % (PI, ' ' * (8 - len(PI)))
     functions = []
 
     for op_str, op in OP_MAP.iteritems():

+ 10 - 2
src/rules/__init__.py

@@ -1,4 +1,5 @@
-from ..node import OP_ADD, OP_MUL, OP_DIV, OP_POW, OP_NEG
+from ..node import OP_ADD, OP_MUL, OP_DIV, OP_POW, OP_NEG, OP_SIN, OP_COS, \
+        OP_TAN
 from .groups import match_combine_groups
 from .factors import match_expand
 from .powers import match_add_exponents, match_subtract_exponents, \
@@ -12,10 +13,12 @@ from .fractions import match_constant_division, match_add_constant_fractions, \
 from .negation import match_negated_factor, match_negate_polynome, \
         match_negated_division
 from .sort import match_sort_multiplicants
+from .goniometry import match_add_quadrants, match_negated_parameter, \
+        match_half_pi_subtraction, match_standard_radian
 
 RULES = {
         OP_ADD: [match_add_numerics, match_add_constant_fractions,
-                 match_combine_groups],
+                 match_combine_groups, match_add_quadrants],
         OP_MUL: [match_multiply_numerics, match_expand, match_add_exponents,
                  match_expand_and_add_fractions, match_multiply_zero,
                  match_negated_factor, match_multiply_one,
@@ -26,4 +29,9 @@ RULES = {
                  match_remove_negative_exponent, match_exponent_to_root,
                  match_extend_exponent, match_constant_exponent],
         OP_NEG: [match_negate_polynome],
+        OP_SIN: [match_negated_parameter, match_half_pi_subtraction,
+                 match_standard_radian],
+        OP_COS: [match_negated_parameter, match_half_pi_subtraction,
+                 match_standard_radian],
+        OP_TAN: [match_standard_radian],
         }

+ 134 - 3
src/rules/goniometry.py

@@ -1,5 +1,6 @@
+from .utils import is_fraction
 from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, OP_ADD, \
-        OP_POW, OP_MUL, OP_SIN, OP_COS, OP_TAN
+        OP_POW, OP_MUL, OP_DIV, OP_SIN, OP_COS, OP_TAN, PI, TYPE_OPERATOR
 from ..possibilities import Possibility as P, MESSAGES
 from ..translate import _
 
@@ -18,7 +19,7 @@ def tan(*args):
 
 def match_add_quadrants(node):
     """
-    sin(x) ^ 2 + cos(x) ^ 2  ->  1
+    sin(t) ^ 2 + cos(t) ^ 2  ->  1
     """
     assert node.is_op(OP_ADD)
 
@@ -36,9 +37,139 @@ def match_add_quadrants(node):
 
 def add_quadrants(root, args):
     """
-    sin(x) ^ 2 + cos(x) ^ 2  ->  1
+    sin(t) ^ 2 + cos(t) ^ 2  ->  1
     """
     return L(1)
 
 
 MESSAGES[add_quadrants] = _('Add the sinus and cosinus quadrants to 1.')
+
+
+def match_negated_parameter(node):
+    """
+    sin(-t)  ->  -sin(t)
+    cos(-t)  ->  cos(t)
+    """
+    assert node.is_op(OP_SIN) or node.is_op(OP_COS)
+
+    t = node[0]
+
+    if t.negated:
+        if node.op == OP_SIN:
+            return [P(node, negated_sinus_parameter, (t,))]
+
+        return [P(node, negated_cosinus_parameter, (t,))]
+
+    return []
+
+
+def negated_sinus_parameter(root, args):
+    """
+    sin(-t)  ->  -sin(t)
+    """
+    return -sin(+args[0])
+
+
+MESSAGES[negated_sinus_parameter] = \
+        _('Bring the negation from the sinus parameter {1} to the outside.')
+
+
+def negated_cosinus_parameter(root, args):
+    """
+    cos(-t)  ->  cos(t)
+    """
+    return cos(+args[0])
+
+
+MESSAGES[negated_cosinus_parameter] = \
+        _('Remove the negation from the cosinus parameter {1}.')
+
+
+def match_half_pi_subtraction(node):
+    """
+    sin(pi / 2 - t)  ->  cos(t)
+    cos(pi / 2 - t)  ->  sin(t)
+    """
+    assert node.is_op(OP_SIN) or node.is_op(OP_COS)
+
+    if node[0].is_op(OP_ADD):
+        half_pi, t = node[0]
+
+        if half_pi == L(PI) / 2:
+            if node.op == OP_SIN:
+                return [P(node, half_pi_subtraction_sinus, (t,))]
+
+    return []
+
+
+def is_pi_frac(node, denominator):
+    """
+    Check if a node is a fraction of 1 multiplied with PI.
+
+    Example:
+    >>> print is_pi_frac(L(1) / 2 * L(PI), 2)
+    True
+    """
+    if not node.is_op(OP_MUL):
+        return False
+
+    frac, pi = node
+
+    if not frac.is_op(OP_DIV) or not pi.is_leaf or pi.value != PI:
+        return False
+
+    n, d = frac
+
+    return n == 1 and d == denominator
+
+
+def sqrt(value):
+    return N('sqrt', L(value))
+
+
+l0, l1, sq2, sq3 = L(0), L(1), sqrt(2), sqrt(3)
+half = l1 / 2
+
+CONSTANTS = {
+    OP_SIN: [l0, half, half * sq2, half * sq3, l1],
+    OP_COS: [l1, half * sq3, half * sq2, half, l0],
+    OP_TAN: [l0, l1 / 3 * sq3, l1, sq3]
+}
+
+
+def match_standard_radian(node):
+    """
+    Apply a direct constant calculation from the constants table.
+
+        | 0 | pi / 6    | pi / 4    | pi / 3    | pi / 2
+    ----+---+-----------+-----------+-----------+-------
+    sin | 0 | 1/2       | sqrt(2)/2 | sqrt(3)/2 | 1
+    cos | 1 | sqrt(3)/2 | sqrt(2)/2 | 1/2       | 0
+    tan | 0 | sqrt(3)/3 | 1         | sqrt(3)   | -
+    """
+    assert node.type == TYPE_OPERATOR and node.op in (OP_SIN, OP_COS, OP_TAN)
+
+    t = node[0]
+
+    if t == 0:
+        return [P(node, standard_radian, (node.op, 0))]
+
+    denoms = [6, 4, 3]
+
+    if node.op != OP_TAN:
+        denoms.append(2)
+
+    for i, denominator in enumerate(denoms):
+        if is_pi_frac(t, denominator):
+            return [P(node, standard_radian, (node.op, i + 1))]
+
+    return []
+
+
+def standard_radian(root, args):
+    op, column = args
+
+    return CONSTANTS[op][column].clone()
+
+
+MESSAGES[standard_radian] = _('Replace standard radian {0}.')

+ 34 - 0
src/rules/utils.py

@@ -1,3 +1,6 @@
+from ..node import ExpressionLeaf as L, OP_MUL, OP_DIV
+
+
 def gcd(a, b):
     """
     Return greatest common divisor using Euclid's Algorithm.
@@ -20,3 +23,34 @@ def least_common_multiple(*args):
     Return lcm of args.
     """
     return reduce(lcm, args)
+
+
+def is_fraction(node, nominator, denominator):
+    """
+    Check if a node represents the fraction of a given nominator and
+    denominator.
+
+    >>> from ..node import ExpressionLeaf as L
+    >>> l1, l2, a = L('a'), L(1), L(2)
+    >>> is_fraction(a / l2, a, 2)
+    True
+    >>> is_fraction(l1 / l2 * a, a, 2)
+    True
+    >>> is_fraction(l2 / l1 * a, a, 2)
+    False
+    """
+    if node.is_op(OP_DIV):
+        nom, denom = node
+
+        return nom == nominator and denom == denominator
+
+    if node.is_op(OP_MUL):
+        # 1 / denominator * nominator
+        # nominator * 1 / denominator
+        left, right = node
+        fraction = L(1) / denominator
+
+        return (left == nominator and right == fraction) \
+               or (right == nominator and left == fraction)
+
+    return False

+ 22 - 0
src/unicode_math.py

@@ -0,0 +1,22 @@
+# vim: set fileencoding=utf-8 :
+SQRT = '√'
+CBRT = '∛'
+FORT = '∜'
+
+PI = 'π'
+INFINITY = '∞'
+
+SUP = {
+    '0': '⁰',
+    '1': '¹',
+    '2': '²',
+    '3': '³',
+    '3': '⁴',
+    '5': '⁵',
+    '6': '⁶',
+    '7': '⁷',
+    '8': '⁸',
+    '9': '⁹',
+}
+
+DOT = '⋅'

+ 59 - 2
tests/test_rules_goniometry.py

@@ -1,14 +1,71 @@
-from src.rules.goniometry import match_add_quadrants, add_quadrants
+# vim: set fileencoding=utf-8 :
+from src.rules.goniometry import sin, cos, tan, match_add_quadrants, \
+        add_quadrants, match_negated_parameter, negated_sinus_parameter, \
+        negated_cosinus_parameter, match_standard_radian, standard_radian, \
+        is_pi_frac
+from src.node import PI, OP_SIN, OP_COS, OP_TAN
 from src.possibilities import Possibility as P
 from tests.rulestestcase import RulesTestCase, tree
+from src.rules import goniometry
+import doctest
 
 
 class TestRulesGoniometry(RulesTestCase):
 
+    def test_doctest(self):
+        self.assertEqual(doctest.testmod(m=goniometry)[0], 0)
+
     def test_match_add_quadrants(self):
-        root = tree('sin x ^ 2 + cos x ^ 2')
+        root = tree('sin t ^ 2 + cos t ^ 2')
         possibilities = match_add_quadrants(root)
         self.assertEqualPos(possibilities, [P(root, add_quadrants, ())])
 
     def test_add_quadrants(self):
         self.assertEqual(add_quadrants(None, ()), 1)
+
+    def test_match_negated_parameter(self):
+        s, c = tree('sin -t, cos -t')
+        t = s[0]
+
+        self.assertEqualPos(match_negated_parameter(s), \
+                [P(s, negated_sinus_parameter, (t,))])
+
+        self.assertEqualPos(match_negated_parameter(c), \
+                [P(c, negated_cosinus_parameter, (t,))])
+
+    def test_negated_sinus_parameter(self):
+        s = tree('sin -t')
+        t = s[0]
+        self.assertEqual(negated_sinus_parameter(s, (t,)), -sin(+t))
+
+    def test_negated_cosinus_parameter(self):
+        c = tree('cos -t')
+        t = c[0]
+        self.assertEqual(negated_cosinus_parameter(c, (t,)), cos(+t))
+
+    def test_is_pi_frac(self):
+        l1, pi = tree('1,' + PI)
+
+        self.assertTrue(is_pi_frac(l1 / 2 * pi, 2))
+        self.assertFalse(is_pi_frac(l1 / 2 * pi, 3))
+        self.assertFalse(is_pi_frac(l1 * pi, 3))
+
+    def test_match_standard_radian(self):
+        s, c, t = tree('sin(1 / 6 * pi), cos(1 / 2 * pi), tan(0)')
+
+        self.assertEqualPos(match_standard_radian(s), \
+                [P(s, standard_radian, (OP_SIN, 1))])
+
+        self.assertEqualPos(match_standard_radian(c), \
+                [P(c, standard_radian, (OP_COS, 4))])
+
+        self.assertEqualPos(match_standard_radian(t), \
+                [P(t, standard_radian, (OP_TAN, 0))])
+
+    def test_standard_radian(self):
+        l0, l1, sq3, pi6, pi4, pi2 = tree('0,1,sqrt(3),1/6*pi,1/4*pi,1/2*pi')
+
+        self.assertEqual(standard_radian(sin(pi6), (OP_SIN, 1)), l1 / 2)
+        self.assertEqual(standard_radian(sin(pi2), (OP_SIN, 4)), 1)
+        self.assertEqual(standard_radian(cos(l0), (OP_COS, 0)), 1)
+        self.assertEqual(standard_radian(tan(pi4), (OP_TAN, 3)), sq3)

+ 10 - 1
tests/test_rules_utils.py

@@ -1,6 +1,7 @@
 import unittest
 
-from src.rules.utils import least_common_multiple
+from src.rules.utils import least_common_multiple, is_fraction
+from tests.rulestestcase import tree
 
 
 class TestRulesUtils(unittest.TestCase):
@@ -9,3 +10,11 @@ class TestRulesUtils(unittest.TestCase):
         self.assertEqual(least_common_multiple(5, 6), 30)
         self.assertEqual(least_common_multiple(5, 6, 15), 30)
         self.assertEqual(least_common_multiple(2, 4), 4)
+
+    def test_is_fraction(self):
+        l1, a = tree('1, a')
+
+        self.assertTrue(is_fraction(a / 2, a, 2))
+        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))