Spreadsheet: safer formula evaluation
This commit is contained in:
parent
1e1786fd41
commit
b4618e3b0a
|
@ -24,6 +24,176 @@ import re, math, FreeCAD, FreeCADGui
|
|||
from PyQt4 import QtCore,QtGui
|
||||
DEBUG = True # set to True to show debug messages
|
||||
|
||||
|
||||
class MathParser:
|
||||
"A math expression parser"
|
||||
# code borrowed from http://www.nerdparadise.com/tech/python/parsemath/
|
||||
def __init__(self, string, vars={}):
|
||||
self.string = string
|
||||
self.index = 0
|
||||
self.vars = {
|
||||
'pi' : math.pi,
|
||||
'e' : math.e
|
||||
}
|
||||
for var in vars.keys():
|
||||
if self.vars.get(var) != None:
|
||||
raise Exception("Cannot redefine the value of " + var)
|
||||
self.vars[var] = vars[var]
|
||||
|
||||
def getValue(self):
|
||||
value = self.parseExpression()
|
||||
self.skipWhitespace()
|
||||
if self.hasNext():
|
||||
raise Exception(
|
||||
"Unexpected character found: '" +
|
||||
self.peek() +
|
||||
"' at index " +
|
||||
str(self.index))
|
||||
return value
|
||||
|
||||
def peek(self):
|
||||
return self.string[self.index:self.index + 1]
|
||||
|
||||
def hasNext(self):
|
||||
return self.index < len(self.string)
|
||||
|
||||
def skipWhitespace(self):
|
||||
while self.hasNext():
|
||||
if self.peek() in ' \t\n\r':
|
||||
self.index += 1
|
||||
else:
|
||||
return
|
||||
|
||||
def parseExpression(self):
|
||||
return self.parseAddition()
|
||||
|
||||
def parseAddition(self):
|
||||
values = [self.parseMultiplication()]
|
||||
while True:
|
||||
self.skipWhitespace()
|
||||
char = self.peek()
|
||||
if char == '+':
|
||||
self.index += 1
|
||||
values.append(self.parseMultiplication())
|
||||
elif char == '-':
|
||||
self.index += 1
|
||||
values.append(-1 * self.parseMultiplication())
|
||||
else:
|
||||
break
|
||||
return sum(values)
|
||||
|
||||
def parseMultiplication(self):
|
||||
values = [self.parseParenthesis()]
|
||||
while True:
|
||||
self.skipWhitespace()
|
||||
char = self.peek()
|
||||
if char == '*':
|
||||
self.index += 1
|
||||
values.append(self.parseParenthesis())
|
||||
elif char == '/':
|
||||
div_index = self.index
|
||||
self.index += 1
|
||||
denominator = self.parseParenthesis()
|
||||
if denominator == 0:
|
||||
raise Exception(
|
||||
"Division by 0 kills baby whales (occured at index " +
|
||||
str(div_index) +
|
||||
")")
|
||||
values.append(1.0 / denominator)
|
||||
else:
|
||||
break
|
||||
value = 1.0
|
||||
for factor in values:
|
||||
value *= factor
|
||||
return value
|
||||
|
||||
def parseParenthesis(self):
|
||||
self.skipWhitespace()
|
||||
char = self.peek()
|
||||
if char == '(':
|
||||
self.index += 1
|
||||
value = self.parseExpression()
|
||||
self.skipWhitespace()
|
||||
if self.peek() != ')':
|
||||
raise Exception(
|
||||
"No closing parenthesis found at character "
|
||||
+ str(self.index))
|
||||
self.index += 1
|
||||
return value
|
||||
else:
|
||||
return self.parseNegative()
|
||||
|
||||
def parseNegative(self):
|
||||
self.skipWhitespace()
|
||||
char = self.peek()
|
||||
if char == '-':
|
||||
self.index += 1
|
||||
return -1 * self.parseParenthesis()
|
||||
else:
|
||||
return self.parseValue()
|
||||
|
||||
def parseValue(self):
|
||||
self.skipWhitespace()
|
||||
char = self.peek()
|
||||
if char in '0123456789.':
|
||||
return self.parseNumber()
|
||||
else:
|
||||
return self.parseVariable()
|
||||
|
||||
def parseVariable(self):
|
||||
self.skipWhitespace()
|
||||
var = ''
|
||||
while self.hasNext():
|
||||
char = self.peek()
|
||||
if char.lower() in '_abcdefghijklmnopqrstuvwxyz0123456789':
|
||||
var += char
|
||||
self.index += 1
|
||||
else:
|
||||
break
|
||||
|
||||
value = self.vars.get(var, None)
|
||||
if value == None:
|
||||
raise Exception(
|
||||
"Unrecognized variable: '" +
|
||||
var +
|
||||
"'")
|
||||
return float(value)
|
||||
|
||||
def parseNumber(self):
|
||||
self.skipWhitespace()
|
||||
strValue = ''
|
||||
decimal_found = False
|
||||
char = ''
|
||||
|
||||
while self.hasNext():
|
||||
char = self.peek()
|
||||
if char == '.':
|
||||
if decimal_found:
|
||||
raise Exception(
|
||||
"Found an extra period in a number at character " +
|
||||
str(self.index) +
|
||||
". Are you European?")
|
||||
decimal_found = True
|
||||
strValue += '.'
|
||||
elif char in '0123456789':
|
||||
strValue += char
|
||||
else:
|
||||
break
|
||||
self.index += 1
|
||||
|
||||
if len(strValue) == 0:
|
||||
if char == '':
|
||||
raise Exception("Unexpected end found")
|
||||
else:
|
||||
raise Exception(
|
||||
"I was expecting to find a number at character " +
|
||||
str(self.index) +
|
||||
" but instead I found a '" +
|
||||
char +
|
||||
"'. What's up with that?")
|
||||
|
||||
return float(strValue)
|
||||
|
||||
class Spreadsheet(object):
|
||||
"""An object representing a spreadsheet. Can be used as a
|
||||
FreeCAD object or as a standalone python object.
|
||||
|
@ -78,23 +248,11 @@ class Spreadsheet(object):
|
|||
if key.lower() in self._cells:
|
||||
key = key.lower()
|
||||
if self.isFunction(self._cells[key]):
|
||||
#print "result = ",self.getFunction(key)
|
||||
# building a list of safe functions allowed in eval
|
||||
safe_list = ['acos', 'asin', 'atan', 'atan2', 'ceil',
|
||||
'cos', 'cosh', 'e', 'exp', 'fabs',
|
||||
'floor', 'fmod', 'frexp', 'hypot', 'ldexp', 'log',
|
||||
'log10', 'modf', 'pi', 'pow', 'radians', 'sin',
|
||||
'sinh', 'sqrt', 'tan', 'tanh']
|
||||
tools = dict((k, getattr(math, k)) for k in safe_list)
|
||||
# adding abs
|
||||
tools["abs"] = abs
|
||||
# removing all builtins from allowed functions
|
||||
tools["__builtins__"] = None
|
||||
try:
|
||||
e = eval(self._format(key),tools,{"self":self})
|
||||
try:
|
||||
e = self.evaluate(key)
|
||||
except:
|
||||
if DEBUG: print "Error evaluating formula"
|
||||
return self._cells[key]
|
||||
print "Error evaluating formula"
|
||||
return None
|
||||
else:
|
||||
return e
|
||||
else:
|
||||
|
@ -123,18 +281,6 @@ class Spreadsheet(object):
|
|||
if self.isFunction(key):
|
||||
self._updateDependencies(key)
|
||||
|
||||
def _format(self,key):
|
||||
"formats all cellnames in the function a the given cell"
|
||||
elts = re.split(r'(\W+)',self._cells[key][1:])
|
||||
#print elts
|
||||
result = ''
|
||||
for e in elts:
|
||||
if self.isKey(e):
|
||||
result += "self."+e
|
||||
else:
|
||||
result += e
|
||||
return result
|
||||
|
||||
def _updateDependencies(self,key,value=None):
|
||||
"search for ancestors in the value and updates the table"
|
||||
ancestors = []
|
||||
|
@ -162,6 +308,17 @@ class Spreadsheet(object):
|
|||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def isNumeric(self,key):
|
||||
"isNumeric(cell): returns True if the given cell returns a number"
|
||||
if self.isFunction(key):
|
||||
res = self.evaluate(key)
|
||||
else:
|
||||
res = self._cells[key]
|
||||
if isinstance(res,float) or isinstance(res,int):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def isKey(self,value):
|
||||
"isKey(val): returns True if the given value is a valid cell number"
|
||||
|
@ -222,6 +379,31 @@ class Spreadsheet(object):
|
|||
if index in [c,r]:
|
||||
cells[k] = self._cells[k]
|
||||
return cells
|
||||
|
||||
def evaluate(self,key):
|
||||
"evaluate(key): evaluates the given formula"
|
||||
elts = re.split(r'(\W+)',self._cells[key][1:])
|
||||
result = ""
|
||||
for e in elts:
|
||||
if self.isKey(e):
|
||||
if self.isFunction(e):
|
||||
if self.isNumeric(e):
|
||||
result += str(self.evaluate(e))
|
||||
else:
|
||||
print "Error evaluating formula"
|
||||
return
|
||||
elif self.isNumeric(e):
|
||||
result += str(self._cells[e])
|
||||
else:
|
||||
result += e
|
||||
print "Evaluating ",result
|
||||
try:
|
||||
p = MathParser(result)
|
||||
result = p.getValue()
|
||||
except Exception as (ex):
|
||||
msg = ex.message
|
||||
raise Exception(msg)
|
||||
return result
|
||||
|
||||
|
||||
class ViewProviderSpreadsheet(object):
|
||||
|
|
Loading…
Reference in New Issue
Block a user