# 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])