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
|
from PyQt4 import QtCore,QtGui
|
||||||
DEBUG = True # set to True to show debug messages
|
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):
|
class Spreadsheet(object):
|
||||||
"""An object representing a spreadsheet. Can be used as a
|
"""An object representing a spreadsheet. Can be used as a
|
||||||
FreeCAD object or as a standalone python object.
|
FreeCAD object or as a standalone python object.
|
||||||
|
@ -78,23 +248,11 @@ class Spreadsheet(object):
|
||||||
if key.lower() in self._cells:
|
if key.lower() in self._cells:
|
||||||
key = key.lower()
|
key = key.lower()
|
||||||
if self.isFunction(self._cells[key]):
|
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:
|
try:
|
||||||
e = eval(self._format(key),tools,{"self":self})
|
e = self.evaluate(key)
|
||||||
except:
|
except:
|
||||||
if DEBUG: print "Error evaluating formula"
|
print "Error evaluating formula"
|
||||||
return self._cells[key]
|
return None
|
||||||
else:
|
else:
|
||||||
return e
|
return e
|
||||||
else:
|
else:
|
||||||
|
@ -123,18 +281,6 @@ class Spreadsheet(object):
|
||||||
if self.isFunction(key):
|
if self.isFunction(key):
|
||||||
self._updateDependencies(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):
|
def _updateDependencies(self,key,value=None):
|
||||||
"search for ancestors in the value and updates the table"
|
"search for ancestors in the value and updates the table"
|
||||||
ancestors = []
|
ancestors = []
|
||||||
|
@ -163,6 +309,17 @@ class Spreadsheet(object):
|
||||||
else:
|
else:
|
||||||
return False
|
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):
|
def isKey(self,value):
|
||||||
"isKey(val): returns True if the given value is a valid cell number"
|
"isKey(val): returns True if the given value is a valid cell number"
|
||||||
allowMoreThanOneLetter = False
|
allowMoreThanOneLetter = False
|
||||||
|
@ -223,6 +380,31 @@ class Spreadsheet(object):
|
||||||
cells[k] = self._cells[k]
|
cells[k] = self._cells[k]
|
||||||
return cells
|
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):
|
class ViewProviderSpreadsheet(object):
|
||||||
def __init__(self, vobj):
|
def __init__(self, vobj):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user