Sources


Sources

Over time, after straightening things out, I will upload sources to the program PyQRS. A small auxiliary file is calc.py. It containts a function evaluate() which parses a string containing a numerical expression and returns its value.

Note: ascii-diagrams were constructed using https://metacpan.org/pod/App::Asciio

import math

class CalcError(Exception): pass

def evaluate(text):
    '''
    input: a string containing a numerical expression
    output: the expression's value
    example: evaluate('3 + cos(pi) / log(e)'); result = 2.0
    Program structure is based on
        N. Wirth: Compilerbau, Eine Einführung. B.G.Teubner, Stuttgart 1981.
    evaluate() is used instead of Python's eval() due to eval's security issues
    '''
    variables = {'pi': 4*math.atan(1), 'e': math.exp(1)}
    extended_numchars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                         '+', '-', '.'}
    lbracket = {'(', '{', '['}
    rbracket = {')', '}', ']'}
    lentext = len(text)    
    pos = 0

    def skipspaces():
        '''
        if pos points to a space: skip until pos points to a non-space character
        '''
        nonlocal pos
        while pos < lentext and text[pos] == ' ':
            pos += 1

    def nextchar():
        '''
        advance one position and skip spaces
        '''
        nonlocal pos
        if pos < lentext:
            pos += 1
            if pos < lentext:
                skipspaces()

    def char():
        '''
        return next character
        '''
        nonlocal pos
        if pos < lentext:
            return text[pos]
        else:
            return None

    def retrieve_number():
        '''
        return next number
        '''
        nonlocal pos
        pos0 = pos
        numchars = extended_numchars - {'+', '-'}
        while pos < lentext and text[pos] in numchars:
            if char() == '.': numchars = numchars - {'. '}
            pos += 1
        value = eval(text[pos0:pos])
        skipspaces()
        return value

    def retrieve_name():
        '''
        return name of a variable or function (pi, e, log, sin, etc.)
        '''
        nonlocal pos
        pos0 = pos
        while pos<lentext and text[pos0:pos+1].isidentifier():
            pos += 1
        name = text[pos0:pos]
        skipspaces()
        return name

    def expression():
        '''
                   .---.
               .-->| + |---.
               |   '---'   v   .------.
            ------------------>| term |------------------>
               |   .---.   ^   '------'            |
               '-->| - |---'      ^        .---.   |
                   '---'          |<-------| + |<--|
                                  |        '---'   |
                                  |        .---.   |
                                  '------- | - |<--'
                                           '---'
        '''

        def term():
            '''
                        .--------.
                   ---->| factor | --------------------->
                        '--------'                 |
                            ^           .---.      |
                            |<----------| * |<-----|
                            |           '---'      |
                            |           .---.      |
                            '-----------| / |<-----'
                                        '---'
            '''

            def factor():
                '''
                     .-------.    .---.
                ---->| power |--->| ! |----->
                     '-------'    '---'
                         |        .---.     .--------.
                         '------->| ^ |---->| factor |----->
                                  '---'     '--------'
                '''

                def power():
                    '''
                     .--------.
                    / next sym \  yes     .-----.
                   (starts with )-------->| log |------>
                    \  alpha   /    |     '-----'
                     '--------'     |     .------.
                       no |         '---->| sqrt |------>
                          |         |     '------'
                          |         |     .----------.
                          |         '---->| sin etc. |------>
                          |               '----------'
                          |
                        .---. yes  .--------.
                       ( num )---->| number |------>
                        '---'      '--------'
                        no|
                          |    .---.    .------------.    .---.
                          '--->| ( |--->| expression |--->| ) |------>
                               '---'    '------------'    '---'
                    '''

                    # power()
                    if char().isalpha():
                        name = retrieve_name()
                        if name in {'log', 'ln'}:
                            val = expression()
                            return math.log(val)
                        elif name == 'sqrt':
                            val = expression()
                            return math.sqrt(val)
                        elif name == 'sin':
                            val = expression()
                            return math.sin(val)
                        elif name == 'cos':
                            val = expression()
                            return math.cos(val)
                        elif name in {'tan', 'tg'}:
                            val = expression()
                            return math.tan(val)
                        elif name in variables:
                            return variables[name] # value in variables dictionary
                        else:
                            raise CalcError('Error in variable:','no variable recognised')
                    elif char() in extended_numchars:
                        val = retrieve_number()
                        return val
                    else:
                        if char() in lbracket:
                            nextchar()
                            val = expression()
                            skipspaces()
                            if not char() in rbracket:
                                raise CalcError('Input error:','no closing right bracket')
                            nextchar()
                            return val
                        else:
                            return None

                # factor():
                p = power()
                f = p
                if char() == '!':
                    f = math.factorial(p)
                    nextchar()
                if char() == '^':
                    nextchar()
                    p = factor()
                    f = math.exp(p * math.log(f))
                return f                                    

            # term():
            f = factor()
            while char() in {'*', '/'}:
                if char() == '*':
                    nextchar()
                    f = f * factor()
                else:
                    nextchar()
                    f = f / factor()
            return f

        # expression():
        skipspaces()
        if char() == '-':
            nextchar()
            t = - term()
        elif char() == '+':
            nextchar()
            t = term()
        else: # no explicit sign
            t = term()
        skipspaces() 
        while char() in {'+', '-'}:
            if char() == '+':
                nextchar()
                t = t + term()
            else:
                nextchar()
                t = t - term()
        return t        

    # evaluate():
    val = expression()
    if pos == lentext:
        return val
    else:
        raise CalcError('Error in expression:', 'nonnumerical symbol')

if __name__ == "__main__":
    try:
        str = '3 + cos(pi) / log(e)'
        print(str + ' =', evaluate(str)) # result = 2.0
#        result = evaluate('3 + cos(pi') # should raise a CalcError
#        result = evaluate('sqrt(-1)') # should raise a ValueError
#        result = evaluate('1/0') # should raise a ZeroDivisionError
    except ValueError:
        print('negative argument?')
    except ZeroDivisionError:
        print('division by zero')
    except CalcError as err:
        print(err.args[0], err.args[1])