Explorar o código

Implemented basic symbolic equations and added Sympy to deps.

Sander Mathijs van Veen %!s(int64=14) %!d(string=hai) anos
pai
achega
474ea0edd8
Modificáronse 7 ficheiros con 175 adicións e 54 borrados
  1. 1 0
      README
  2. 59 10
      src/calc.py
  3. 3 0
      src/default_config.py
  4. 9 1
      src/logger.py
  5. 71 0
      tests/parser.py
  6. 4 43
      tests/test_calc.py
  7. 28 0
      tests/test_variables.py

+ 1 - 0
README

@@ -26,6 +26,7 @@ Dependencies
  - GNU Bison and Flex.
  - GNU GCC (or another decent C compiler).
  - Pyrex <http://www.cosc.canterbury.ac.nz/greg.ewing/python/Pyrex/>.
+ - Sympy <https://github.com/sympy/sympy>.
 
 In order to run the unit tests and and see how much coverage the unit tests
 have, please install:

+ 59 - 10
src/calc.py

@@ -3,6 +3,9 @@
 A simple pybison parser program implementing a calculator
 """
 
+from sympy import Symbol
+from logger import filter_non_ascii
+
 import os.path
 PYBISON_BUILD = os.path.realpath('build/external/pybison')
 PYBISON_PYREX = os.path.realpath('external/pybison/src/pyrex')
@@ -25,7 +28,7 @@ class Parser(BisonParser):
     # ----------------------------------------------------------------
     # lexer tokens - these must match those in your lex script (below)
     # ----------------------------------------------------------------
-    tokens = ['NUMBER',
+    tokens = ['NUMBER', 'IDENTIFIER',
               'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'POW',
               'LPAREN', 'RPAREN',
               'NEWLINE', 'QUIT']
@@ -51,7 +54,7 @@ class Parser(BisonParser):
     # ------------------------------------------------------------------
     def read(self, nbytes):
         try:
-            return raw_input(">>> ") + "\n"
+            return raw_input('>>> ') + "\n"
         except EOFError:
             return ''
 
@@ -75,8 +78,12 @@ class Parser(BisonParser):
         """
 
         if option == 1:
+            # Interactive mode is enabled if the term rewriting system is used
+            # as a shell. In that case, it is useful that the shell prints the
+            # output of the evaluation.
             if self.interactive:
                 print values[1]
+
             return values[1]
 
     def on_line(self, target, option, names, values):
@@ -84,7 +91,7 @@ class Parser(BisonParser):
         line : NEWLINE
              | exp NEWLINE
         """
-        if option == 1:
+        if option in [1, 2]:
             if self.verbose:
                 print 'on_line: exp =', values[0]
 
@@ -93,6 +100,7 @@ class Parser(BisonParser):
     def on_exp(self, target, option, names, values):
         """
         exp : NUMBER
+            | IDENTIFIER
             | exp PLUS exp
             | exp MINUS exp
             | exp TIMES exp
@@ -100,41 +108,81 @@ class Parser(BisonParser):
             | MINUS exp %prec NEG
             | exp POW exp
             | LPAREN exp RPAREN
+            | symbolic
         """
 
         if self.verbose:
             print 'on_exp: got %s %s %s %s' % (target, option, names, values)
 
+        # rule: NUMBER
         if option == 0:
             # TODO: A bit hacky, this achieves long integers and floats.
             # return float(values[0]) if '.' in values[0] else long(values[0])
             return float(values[0])
 
-        if option == 7:
+        # rule: IDENTIFIER
+        if option == 1:
+            return Symbol(values[0])
+
+        # rule: LPAREN exp RPAREN
+        if option == 8:
+            return values[1]
+
+        # rule: symbolic
+        if option == 9:
             return values[1]
 
         try:
-            if option == 1:
+            # rule: exp PLUS expo
+            if option == 2:
                 return values[0] + values[2]
 
-            if option == 2:
+            # rule: exp MINUS expo
+            if option == 3:
                 return values[0] - values[2]
 
-            if option == 3:
+            # rule: exp TIMES expo
+            if option == 4:
                 return values[0] * values[2]
 
-            if option == 4:
+            # rule: exp DIVIDE expo
+            if option == 5:
                 return values[0] / values[2]
 
-            if option == 5:
+            # rule: NEG expo
+            if option == 6:
                 return - values[1]
 
-            if option == 6:
+            # rule: exp POW expo
+            if option == 7:
                 return values[0] ** values[2]
         except OverflowError:
             print >>sys.stderr, 'error: Overflow occured in "%s" %s %s %s' \
                                 % (target, option, names, values)
 
+    def on_symbolic(self, target, option, names, values):
+        """
+        symbolic : NUMBER IDENTIFIER
+                 | IDENTIFIER NUMBER
+                 | IDENTIFIER IDENTIFIER
+        """
+        # TODO: this class method requires verification.
+
+        # rule: NUMBER IDENTIFIER
+        if option == 0:
+            # 4x -> 4*x
+            return values[0] * Symbol(values[1])
+
+        # rule: IDENTIFIER NUMBER
+        if option == 1:
+            # x4 -> x^4
+            return Symbol(values[0]) ** values[1]
+
+        # rule: IDENTIFIER IDENTIFIER
+        if option == 2:
+            # a b -> a * b
+            return Symbol(values[0]) * Symbol(values[1])
+
     # -----------------------------------------
     # raw lex script, verbatim here
     # -----------------------------------------
@@ -155,6 +203,7 @@ class Parser(BisonParser):
     %%
 
     [0-9]+ { returntoken(NUMBER); }
+    [a-zA-Z][a-zA-Z0-9]* { returntoken(IDENTIFIER); }
     "("    { returntoken(LPAREN); }
     ")"    { returntoken(RPAREN); }
     "+"    { returntoken(PLUS); }

+ 3 - 0
src/default_config.py

@@ -0,0 +1,3 @@
+# Error/debug/information logging facilities.
+LOG_FILE = 'engine.log'
+LOG_FORMAT = '%(asctime)s %(name)-15s %(message)s'

+ 9 - 1
src/logger.py

@@ -2,7 +2,11 @@ import logging
 import logging.config
 import sys
 
-import config
+try:
+    import config
+except ImportError:
+    config = object()
+
 import default_config as default
 
 try:
@@ -16,3 +20,7 @@ except IOError as e:  # pragma: no cover
 
 def logger(name):
     return logging.getLogger(name)
+
+
+def filter_non_ascii(data):
+        return ''.join(map(lambda x: 33 < ord(x) < 125 and x or '.', data))

+ 71 - 0
tests/parser.py

@@ -0,0 +1,71 @@
+import sys
+
+from src.calc import Parser
+
+
+class TestParser(Parser):
+
+    def __init__(self, **kwargs):
+        Parser.__init__(self, **kwargs)
+
+        self.input_buffer = []
+        self.input_position = 0
+
+    def run(self, input_buffer, *args, **kwargs):
+        map(self.append, input_buffer)
+        return Parser.run(self, *args, **kwargs)
+
+    def append(self, input):
+        self.input_buffer.append(input + '\n')
+
+    def read(self, nbytes):
+        buffer = ''
+
+        try:
+            buffer = self.input_buffer[self.input_position]
+
+            if self.verbose:
+                print 'read:', buffer
+        except IndexError:
+            return ''
+
+        self.input_position += 1
+
+        return buffer
+
+
+def run_expressions(expressions, keepfiles=1, fail=True, silent=False,
+        verbose=0):
+    """
+    Run a list of mathematical expression through the term rewriting system and
+    check if the output matches the expected output. The list of EXPRESSIONS
+    consists of tuples (expression, output), where expression is the
+    mathematical expression to evaluate (String) and output is the expected
+    output of the evaluation (thus, the output can be Float, Int or None).
+
+    If KEEPFILES is non-zero or True, the generated Flex and Bison files will
+    be kept. Otherwise, those temporary files will be deleted. If FAIL is True,
+    and the output of the expression is not equal to the expected output, an
+    assertion error is raised. If SILENT is False, and an assertion error is
+    raised, an error message is printed on stderr. If SILENT is True, no error
+    message will be printed.
+
+    If VERBOSE is non-zero and a positive integer number, verbosity of the term
+    rewriting system will be increased. This will output debug messages and a
+    higher value will print more types of debug messages.
+    """
+
+    parser = TestParser(keepfiles=keepfiles, verbose=verbose)
+
+    for exp, out in expressions:
+        res = None
+        try:
+            res = parser.run([exp])
+            assert res == out
+        except:
+            if not silent:
+                print >>sys.stderr, 'error: %s = %s, but expected: %s' \
+                                    % (exp, str(res), str(out))
+
+            if fail:
+                raise

+ 4 - 43
tests/test_calc.py

@@ -1,34 +1,6 @@
-import sys
 import unittest
 
-
-from src.calc import Parser
-
-
-class TestParser(Parser):
-
-    def __init__(self, input_buffer, **kwargs):
-        Parser.__init__(self, **kwargs)
-
-        self.input_buffer = []
-        self.input_position = 0
-
-        map(self.append, input_buffer)
-
-    def append(self, input):
-        self.input_buffer.append(input + '\n')
-
-    def read(self, nbytes):
-        buffer = ''
-
-        try:
-            buffer = self.input_buffer[self.input_position]
-        except IndexError:
-            return ''
-
-        self.input_position += 1
-
-        return buffer
+from tests.parser import TestParser, run_expressions
 
 
 class TestCalc(unittest.TestCase):
@@ -39,19 +11,8 @@ class TestCalc(unittest.TestCase):
     def tearDown(self):
         pass
 
-    def run_expressions(self, expressions, fail=True):
-        for exp, out in expressions:
-            try:
-                res = TestParser([exp], keepfiles=1).run()
-                assert res == out
-            except:
-                print >>sys.stderr, 'error: %s = %s, but expected: %s' \
-                                    % (exp, str(res), str(out))
-                if fail:
-                    raise
-
     def test_constructor(self):
-        assert TestParser(['1+4'], keepfiles=1).run() == 5.0
+        assert TestParser(keepfiles=1).run(['1+4']) == 5.0
 
     def test_basic_on_exp(self):
         expressions = [('4', 4.0),
@@ -62,7 +23,7 @@ class TestCalc(unittest.TestCase):
                        ('3^4', 81.0),
                        ('(4)', 4.0)]
 
-        self.run_expressions(expressions)
+        run_expressions(expressions)
 
     def test_infinity(self):
         expressions = [('2^9999', None),
@@ -70,4 +31,4 @@ class TestCalc(unittest.TestCase):
                        ('2^99999999999', None),
                        ('2^-99999999999', 0.0)]
 
-        self.run_expressions(expressions, fail=False)
+        run_expressions(expressions, fail=False)

+ 28 - 0
tests/test_variables.py

@@ -0,0 +1,28 @@
+import unittest
+
+from tests.parser import TestParser, run_expressions
+from sympy import Symbol, symbols
+
+
+class TestVariables(unittest.TestCase):
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_addition(self):
+        a = Symbol('a')
+        expressions = [('a + 5', a + 5)]
+        run_expressions(expressions)
+
+    def test_addition_of_two_terms(self):
+        a, b = symbols('a,b')
+        expressions = [('4*a + 5*b', 4.0*a + 5.0*b)]
+        run_expressions(expressions)
+
+    #def test_short_addition_of_two_terms(self):
+    #    a, b = symbols('a,b')
+    #    expressions = [('4a + 5b', 4.0*a + 5.0*b)]
+    #    run_expressions(expressions, verbose=1)