Răsfoiți Sursa

Implemented basic symbolic equations and added Sympy to deps.

Sander Mathijs van Veen 14 ani în urmă
părinte
comite
474ea0edd8
7 a modificat fișierele cu 175 adăugiri și 54 ștergeri
  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 Bison and Flex.
  - GNU GCC (or another decent C compiler).
  - GNU GCC (or another decent C compiler).
  - Pyrex <http://www.cosc.canterbury.ac.nz/greg.ewing/python/Pyrex/>.
  - 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
 In order to run the unit tests and and see how much coverage the unit tests
 have, please install:
 have, please install:

+ 59 - 10
src/calc.py

@@ -3,6 +3,9 @@
 A simple pybison parser program implementing a calculator
 A simple pybison parser program implementing a calculator
 """
 """
 
 
+from sympy import Symbol
+from logger import filter_non_ascii
+
 import os.path
 import os.path
 PYBISON_BUILD = os.path.realpath('build/external/pybison')
 PYBISON_BUILD = os.path.realpath('build/external/pybison')
 PYBISON_PYREX = os.path.realpath('external/pybison/src/pyrex')
 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)
     # lexer tokens - these must match those in your lex script (below)
     # ----------------------------------------------------------------
     # ----------------------------------------------------------------
-    tokens = ['NUMBER',
+    tokens = ['NUMBER', 'IDENTIFIER',
               'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'POW',
               'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'POW',
               'LPAREN', 'RPAREN',
               'LPAREN', 'RPAREN',
               'NEWLINE', 'QUIT']
               'NEWLINE', 'QUIT']
@@ -51,7 +54,7 @@ class Parser(BisonParser):
     # ------------------------------------------------------------------
     # ------------------------------------------------------------------
     def read(self, nbytes):
     def read(self, nbytes):
         try:
         try:
-            return raw_input(">>> ") + "\n"
+            return raw_input('>>> ') + "\n"
         except EOFError:
         except EOFError:
             return ''
             return ''
 
 
@@ -75,8 +78,12 @@ class Parser(BisonParser):
         """
         """
 
 
         if option == 1:
         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:
             if self.interactive:
                 print values[1]
                 print values[1]
+
             return values[1]
             return values[1]
 
 
     def on_line(self, target, option, names, values):
     def on_line(self, target, option, names, values):
@@ -84,7 +91,7 @@ class Parser(BisonParser):
         line : NEWLINE
         line : NEWLINE
              | exp NEWLINE
              | exp NEWLINE
         """
         """
-        if option == 1:
+        if option in [1, 2]:
             if self.verbose:
             if self.verbose:
                 print 'on_line: exp =', values[0]
                 print 'on_line: exp =', values[0]
 
 
@@ -93,6 +100,7 @@ class Parser(BisonParser):
     def on_exp(self, target, option, names, values):
     def on_exp(self, target, option, names, values):
         """
         """
         exp : NUMBER
         exp : NUMBER
+            | IDENTIFIER
             | exp PLUS exp
             | exp PLUS exp
             | exp MINUS exp
             | exp MINUS exp
             | exp TIMES exp
             | exp TIMES exp
@@ -100,41 +108,81 @@ class Parser(BisonParser):
             | MINUS exp %prec NEG
             | MINUS exp %prec NEG
             | exp POW exp
             | exp POW exp
             | LPAREN exp RPAREN
             | LPAREN exp RPAREN
+            | symbolic
         """
         """
 
 
         if self.verbose:
         if self.verbose:
             print 'on_exp: got %s %s %s %s' % (target, option, names, values)
             print 'on_exp: got %s %s %s %s' % (target, option, names, values)
 
 
+        # rule: NUMBER
         if option == 0:
         if option == 0:
             # TODO: A bit hacky, this achieves long integers and floats.
             # 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 '.' in values[0] else long(values[0])
             return float(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]
             return values[1]
 
 
         try:
         try:
-            if option == 1:
+            # rule: exp PLUS expo
+            if option == 2:
                 return values[0] + values[2]
                 return values[0] + values[2]
 
 
-            if option == 2:
+            # rule: exp MINUS expo
+            if option == 3:
                 return values[0] - values[2]
                 return values[0] - values[2]
 
 
-            if option == 3:
+            # rule: exp TIMES expo
+            if option == 4:
                 return values[0] * values[2]
                 return values[0] * values[2]
 
 
-            if option == 4:
+            # rule: exp DIVIDE expo
+            if option == 5:
                 return values[0] / values[2]
                 return values[0] / values[2]
 
 
-            if option == 5:
+            # rule: NEG expo
+            if option == 6:
                 return - values[1]
                 return - values[1]
 
 
-            if option == 6:
+            # rule: exp POW expo
+            if option == 7:
                 return values[0] ** values[2]
                 return values[0] ** values[2]
         except OverflowError:
         except OverflowError:
             print >>sys.stderr, 'error: Overflow occured in "%s" %s %s %s' \
             print >>sys.stderr, 'error: Overflow occured in "%s" %s %s %s' \
                                 % (target, option, names, values)
                                 % (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
     # raw lex script, verbatim here
     # -----------------------------------------
     # -----------------------------------------
@@ -155,6 +203,7 @@ class Parser(BisonParser):
     %%
     %%
 
 
     [0-9]+ { returntoken(NUMBER); }
     [0-9]+ { returntoken(NUMBER); }
+    [a-zA-Z][a-zA-Z0-9]* { returntoken(IDENTIFIER); }
     "("    { returntoken(LPAREN); }
     "("    { returntoken(LPAREN); }
     ")"    { returntoken(RPAREN); }
     ")"    { returntoken(RPAREN); }
     "+"    { returntoken(PLUS); }
     "+"    { 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 logging.config
 import sys
 import sys
 
 
-import config
+try:
+    import config
+except ImportError:
+    config = object()
+
 import default_config as default
 import default_config as default
 
 
 try:
 try:
@@ -16,3 +20,7 @@ except IOError as e:  # pragma: no cover
 
 
 def logger(name):
 def logger(name):
     return logging.getLogger(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
 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):
 class TestCalc(unittest.TestCase):
@@ -39,19 +11,8 @@ class TestCalc(unittest.TestCase):
     def tearDown(self):
     def tearDown(self):
         pass
         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):
     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):
     def test_basic_on_exp(self):
         expressions = [('4', 4.0),
         expressions = [('4', 4.0),
@@ -62,7 +23,7 @@ class TestCalc(unittest.TestCase):
                        ('3^4', 81.0),
                        ('3^4', 81.0),
                        ('(4)', 4.0)]
                        ('(4)', 4.0)]
 
 
-        self.run_expressions(expressions)
+        run_expressions(expressions)
 
 
     def test_infinity(self):
     def test_infinity(self):
         expressions = [('2^9999', None),
         expressions = [('2^9999', None),
@@ -70,4 +31,4 @@ class TestCalc(unittest.TestCase):
                        ('2^99999999999', None),
                        ('2^99999999999', None),
                        ('2^-99999999999', 0.0)]
                        ('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)