-
Notifications
You must be signed in to change notification settings - Fork 0
/
MathExpression.py
262 lines (238 loc) · 11.7 KB
/
MathExpression.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
import operator
import numpy as np
import math
import re
import functools
from threading import Thread
def forceReversible(func):
"""Tries reversing the arguments of the two argument function func if the original raises a TypeError"""
@functools.wraps(func)
def wrapper(arg0, arg1):
print "Arg 0: %s\nArg 1: %s" % (str(arg0), str(arg1))
try:
return func(arg0, arg1)
except TypeError:
return func(arg1, arg0)
return wrapper
class MathExpression:
"""A parser for mathematical expressions. Does not depend on any other modules in dataManipulation.
Allows the specification of a list of operators which are used to break down a string, and which are assigned to a
function which takes exactly two arguments through an OrderedDict. Interpretation is done on *every* part of the
expression, thus making it impossible to execute arbitrary code. In order to maintain "safety," the input must
be a single string. Parsing is completed in groups of 3 in the format token + operator + token. This is done
iteratively. Expressions are recursively broken into sub-expressions through
the use of matching parenthesis, and evaluated from the inside out.
Function calls are evaluated in the format func(arg0, arg1...). Kwargs are not supported. A
backup function in the format backup(func, *args) can be specified to handle arguments which are passed to a
function but are of improper type; for instance, using only the first index of an array as arguments
for certain functions, etc.
"""
__author__ = "Thomas Schweich"
@staticmethod
def returnDict(key, value):
return {key: value}
'''
operators = collections.OrderedDict(
(("(", None), (")", None), (",", None), ("^", forceReversible(operator.pow)), ("/", forceReversible(operator.div)),
("*", forceReversible(operator.mul)), ("+", forceReversible(operator.add)), ("-", forceReversible(operator.sub))))
'''
operators = [{"(": None, ")": None, ",": None}, {"^": forceReversible(operator.pow)},
{"/": forceReversible(operator.div), "*": forceReversible(operator.mul)},
{"+": forceReversible(operator.add), "-": forceReversible(operator.sub)}]
modules = (np, math)
def __init__(self, expression, variables=None, operators=operators, modules=modules, fallbackFunc=None):
self.variables = variables if variables is not None else {}
self.operators = operators if operators is not None else MathExpression.operators
self.modules = modules if modules is not None else MathExpression.modules
self.fallbackFunc = fallbackFunc
self.expression = self.genFromString(expression)
self.loops = 0
self.result = None
def genFromString(self, string):
"""Separates string by .operators using regex"""
# operators = self.operators.keys()
operators = [o for d in self.operators for o in d.keys()] # o: operator, d: dict
operators.sort(key=lambda x: -len(x))
exp = re.findall(r'<.*?>|' + "|".join(["%s" % re.escape(op) for op in operators]) + '|[\.\w]+', string)
print exp
return exp
def getEvaluationThread(self):
"""Returns a Thread who's target is evaluate() which can be started and joined at your leisure"""
return Thread(target=self.evaluate())
def evaluate(self):
"""Calls evaluateExpression() on .expression"""
self.expression = self.evaluateExpression(self.expression)
def evaluateExpression(self, exp):
"""Recursively evaluates expressions starting with innermost parenthesis, working outwards
Iteratively solves sub-expressions (grouped by parenthesis) in the order of .operators
"""
try:
isCompleteExp = len(exp) >= 3
except TypeError:
isCompleteExp = False
if isCompleteExp:
print "-New Starting expression: %s" % str(exp)
rightInner = exp.index(")") if ")" in exp else len(exp)
print "Right inner parenthesis index: %d" % rightInner
leftSide = exp[:rightInner]
leftInner = len(leftSide) - leftSide[::-1].index("(") if "(" in leftSide else 0
print "Left inner parenthesis index: %d" % leftInner
subExp = leftSide[leftInner:]
print "Sub Expression: " + str(subExp)
callerIndex = leftInner - 2
allOps = [o for d in self.operators for o in d.keys()]
if callerIndex > -1 and exp[callerIndex] not in allOps:
print "Calling function...."
# Call function if in format something(arg0, arg1...) if "something" is not an operator
args = []
while "," in subExp:
args.append(self.evaluateExpression(list(subExp[:subExp.index(",")])))
del subExp[:subExp.index(",") + 1]
args.append(self.evaluateExpression(subExp))
# kwargs = {}
'''
for i, arg in enumerate(args):
if isinstance(arg, dict):
kwargs.update(args.pop(i))
'''
print "Arguments: " + str(args)
# print "Kwargs: " + str(kwargs)
funcToCall = self._interpret(exp[callerIndex])
try:
result = funcToCall(*args) # , **kwargs)
except:
try:
result = self.fallbackFunc(funcToCall, *args)
except Exception as e:
raise MathExpression.ParseFailure(str(funcToCall), e)
print "Result: " + str(result)
del exp[callerIndex:rightInner + 1]
exp.insert(callerIndex, result)
print "Expression after replacement" + str(exp)
print "....call complete"
else:
print "Evaluating expression...."
# Otherwise, evaluate the expression within the parenthesis, replacing the range with the result
newExp = subExp[:]
for order in self.operators:
for index, part in enumerate(subExp):
self.loops += 1
if part in order.keys(): # Changing to allow for equal level operators ## ch
newLocation = newExp.index(part)
prevIndex = newLocation - 1
nextIndex = newLocation + 1
try:
prev = self._interpret(newExp[prevIndex])
nxt = self._interpret(newExp[nextIndex])
except IndexError as i:
raise MathExpression.ParseFailure(part, i)
print "Combining %s with %s using '%s' operator" % (str(prev), str(nxt), str(part))
if not (isinstance(prev, np.ndarray) and not isinstance(nxt, np.ndarray)):
# Call the function stored in this order's dict under the operator
solution = order[part](prev, nxt)
else:
raise MathExpression.SyntaxError(prev)
print "Solution: " + str(solution)
del newExp[prevIndex:nextIndex + 1]
newExp.insert(prevIndex, solution)
print "After replacement with solution: " + str(newExp)
try:
hasParens = exp[leftInner - 1] == "(" and exp[rightInner] == ")"
except IndexError:
raise MathExpression.SyntaxError(exp)
if len(newExp) == 1:
if hasParens:
print "Replacing parenthesis and expression"
del exp[leftInner - 1:rightInner + 1]
else:
print "Replacing expression only (parenthesis not found)"
del exp[leftInner:rightInner]
exp.insert(leftInner-1, newExp[0])
else:
raise MathExpression.SyntaxError(newExp)
print "New Expression: %s" % str(exp)
print "....evaluate complete"
print "Length of expression: %d" % len(exp)
return self.evaluateExpression(exp)
else:
if not isinstance(exp, list):
print "Loops: %d" % self.loops
self.loops = 0
return exp
elif len(exp) == 1:
return self._interpret(exp[0])
else:
raise MathExpression.SyntaxError(exp)
def _interpret(self, string):
if isinstance(string, str):
if string[0] == "<" and string[-1] == ">":
varString = string[1:-1]
try:
print "Trying interpret %s as variable" % varString
return self.variables[varString]
except KeyError as k:
raise MathExpression.ParseFailure(string, k)
else:
try:
print "Trying interpret %s as float" % string
return float(string)
except ValueError:
pass
for module in self.modules:
try:
print "Trying interpret %s as %s" % (string, str(module))
return getattr(module, string)
except AttributeError:
pass
raise MathExpression.SyntaxError(string)
# return string
else:
return string
'''
def operate(self, operator_, *args):
# TODO Dict kwargs
kwargs = [self._interpret(arg) for arg in args if isinstance(arg, dict)]
args = [self._interpret(arg) for arg in args if not isinstance(arg, dict)]
return operator_(*args, **kwargs)
'''
class ParseFailure(Exception):
"""Represents the expression group (i.e. token + operator + token) and the given exception"""
def __init__(self, badPart, exception):
self.badPart = badPart
self.exception = exception
def __repr__(self):
custom = ""
if self.exception is AttributeError:
custom += "'%s' not found in namespace. \n" % str(self.badPart)
if self.badPart in MathExpression.operators:
custom += "It is likely that the operator was missing an argument. "
return "%s threw error: %s. %s" % (str(self.badPart), str(self.exception), custom)
def __str__(self):
return str(self.__repr__())
class SyntaxError(Exception):
"""Represents only the expression group (i.e. token + operator + token)"""
def __init__(self, badPart):
self.badPart = badPart
def __repr__(self):
custom = ""
if "(" in self.badPart or ")" in self.badPart:
custom += "It is likely that you are missing a parenthesis."
return "Syntax error on sub expression: %s. \n%s" % (str(self.badPart), custom)
def __str__(self):
return str(self.__repr__())
def limit(max_=None):
"""Return decorator that limits allowed returned values."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
ret = func(*args, **kwargs)
try:
mag = abs(ret)
except TypeError:
pass # not applicable
else:
if mag > max_:
raise ValueError(ret)
return ret
return wrapper
return decorator