• API
• FAQ
• Tools
• Trends
• Archive
SHARE
TWEET

# equation_evaluator.py

piguy123 Mar 26th, 2013 292 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
1. # -*- coding: cp1252 -*-
2. """
3. Mathematical Expression Evaluator
4.
5. Author: Sunjay Varma
6. Website: www.sunjay.ca
7.
8. Supports a wide array of syntaxes and expressions.
9. * For complex numbers, use complex()
10. * use mode(RADIANS) or mode(DEGREES) to change trig mode
11. * You can define functions using 'f(x) =' notation (multivariable support e.g. 'abc(x, y, z) =')
12. * You can define variables with the 2 -> x notation (or more generally, expression -> expression)
13. * You can multiply using 2x and (2)(-4) notation
14. * Vectors are created for you and used as lists of numbers (YOU MUST HAVE THE 'Vector' MODULE.)
15. * 'fib' is an optional module that generates fibonacci numbers (use the fib function)
16. * The evaluator will balance parenthesis for you
17. * You can use |variable_name| for abs(variable_name)
18. * Use ||v|| to find the magnitude of Vector 'v'
19. * You can use ^ for exponents: e.g. f(x) = x^2 is the same as f(x) = x**2
20. * Technically, this supports ± (plus or minus) and square root symbols as well
21.
22. You MUST set local to 'global_scope' when using meval if you want to have all the math functions!
23.
24. It should stop you from trying any real Python expressions...let me know if you find any bugs!
25.
26. General usage:
27. >>> meval('sin(pi/pi-1)', global_scope)
28. 0
29. >>> meval('2x', {'x': 8})
30. 16
31. >>> interact()
32. Type 'exit' or 'quit' to end this interpreter
33. Math> 2 + 2
34. 4
35. Math> 2(4)
36. 8
37. Math> 2(4)^2
38. 32
39. Math> exit
40. """
41.
42. from __future__ import division
43. import cmath, math, re, code
44. from itertools import imap, izip
45. import keyword
46. import signal
47.
48. # regular expressions
49. #r_term_parts = re.compile(r'(\d*)([^\^]+)?')
50. #r_plus_minus = re.compile(r'\+\s*\-')
51. # Used to get rid of all white space
52. r_white_space = re.compile(r'\s+')
53. # Math functions, not python functions: e.g. f(x) = y, abc(x, y, z) = q
54. r_func_def = re.compile(r'([a-zA-Z_]+)\(([^\)]+)\)\=')
55. # Function used for r_func_def to turn math functions into Python ones
56. repl_func_def = lambda m: m.group(1)+' = lambda ' + m.group(2) + ': '
57. # In math, we can do lots of fancy things with multiplication that we can't do in Python
58. # This translates most math multiplication notation into Python
59. r_terms_mul = re.compile(r'''                                         # fix things like 2x and 2(4)
60.                            (\d+                                      # number
61.                            |                                         # or
62.                            [^a-zA-Z\*\/+\-\(\,\=\:><\|!.; \t\n\r\f\v\xb1_]       # anything not a letter or operator
63.                            )
64.                            ([a-zA-Z_\(]+)          # either parenthesis or a letter
65.                            ''', re.VERBOSE)
66. # Catches functions with '__' around them
67. r_py_func = re.compile(r'__([^_]+)__')
68. # Supports assignment through 2 -> x
69. r_assign = re.compile(r'(.+)(?=\-\>)\-\>(.+)')
70. # Finds vectors
71. r_vector = re.compile(r'(?<!\w)(\([^\)]*\))')
72. # Finds angle functions
73. r_angle_func = re.compile(r'(a?sin|cos|tan{1}h?)(\([^\)]+\))')
74. # Supports mathematical abs notation: e.g. |x|
75. r_abs = re.compile(r'\|([^*/]+)\|')
76. # Finds the magnitude of a vector ||v||
77. r_mag = re.compile(r'\|\|([^*/]+)\|\|')
78. #r_mag_mul = re.compile(r'(\|{1,2})(.+\|)\1(\|{1,2})(.+\|)\3')
79. # Finds integers
80. r_int = re.compile(r'(?<!\.)(\d+)(?!\.)')
81. # Finds invalid integers (syntax error)
82. r_invalid_int = re.compile(r'(\d+\.\d+)\.0')
83. # Finds decimal numbers
84. r_deg = re.compile(r"[+-]?((\d+(\.\d*)?)|\.\d+)([eE][+-]?[0-9]+)?\xb0")
85. # Looks for words (variables, function names, etc)
86. r_word = re.compile(r"\w+")
87. # Looks for the square root symbol
88. r_sqrt = re.compile(u"\u221a"+r"(\()?")
89.
90. # constants
91. DEFAULT_VARIABLE = "x"
92. # calculator modes
94. DEGREE = 'DEGREE'
95. MODE_CONST = 'CALC_MODE' # used for detecting modes
97.
98. # errors
99. class Error(Exception): pass # base exception for this module
100. class PotentialSecurityThreat(Error): pass
101. class CalculationFailure(Error): pass
102.
103. # create a global scope which will be used during the evaluation of the expression
104. global_scope = {
105.     '__builtins__': {},
106.     MODE_CONST: DEFAULT_MODE,
108.     'DEGREE': DEGREE
109. }
110.
111. # make sure the user has access to math and cmath variables
112. for mod in [math, cmath]:
113.     global_scope.update(vars(mod))
114.
115. # things that are needed from the builtin functions
116. builtins_needed = {
117.     'abs': abs,
118.     'int': int,
119.     'float': float,
120.     'round': round,
121. }
122.
123. # add specific built in functions (and nothing else)
124. global_scope.update(builtins_needed)
125.
127. for name in 'Vector fib'.split():
128.     try:
129.         global_scope[name] = value = getattr(__import__(name.lower()), name)
130.         exec "global %s"%name
131.         exec "%s = getattr(__import__(%r.lower()), %r)"%(name, name, name)
132.     except:
133.         pass
134.
135. def vrange(start, stop=None, step=1):
136.     """Vector of range(start, stop, step)"""
137.     if stop is None:
138.         stop = start
139.         start = 0
140.     return Vector(range(int(start), int(stop), int(step)))
141.
142. # add this to the global scope so the user has access
143. global_scope["vrange"] = vrange
144.
145. def dextend(*args):
146.     """Update the first argument with all the others"""
147.     ext = args[0]
148.     for d in args[1:]:
149.         ext.update(d)
150.     return ext
151.
152. def make_mode_func(scope):
153.     """Make a function to define the calculator mode"""
154.     def _mode(mode=None):
155.         if mode and mode in [RADIAN, DEGREE]:
156.             scope[MODE_CONST] = mode
157.         return scope[MODE_CONST]
158.     scope['mode'] = _mode
159.
160. def sstartswith(string, prefixes):
161.     """Return True if string starts with any of the prefixes"""
162.     for prefix in prefixes:
163.         if string.startswith(prefix):
164.             return True
165.
166. def sreplace(s, old, new):
167.     """Basically goes through old and replaces old[i] with the corresponding new[i]"""
168.     for x, y in izip(old, new):
169.         s = s.replace(x, y)
170.     return s
171.
172. BRACKETS = dict(izip('[{(', ']})'))
173.
174. def balance(s):
175.     """
176.    Balance the parentheses within a string
177.
178.    sin(cos(tan(x) --> sin(cos(tan(x)))
179.    """
180.     openedb = BRACKETS.keys()
181.     closedb = BRACKETS.values()
182.     if [s.count(x) for x in openedb] == [s.count(x) for x in closedb]:
183.         return s # balanced
184.     open_br = []
185.     for c in s:
186.         if c in openedb:
187.             open_br.append(c)
188.         elif c in closedb:
189.             if not open_br or not BRACKETS[open_br[-1]] == c:
190.                     raise SyntaxError('Invalid Syntax!') # too many closing brackets
191.             open_br.pop()
192.     for br in open_br:
193.         s += BRACKETS[br]
194.     return s
195.
196. def check_security_threat(expr, orig):
197.     """Looks for potential security threats by seeking out python keywords and underscores"""
198.     if sstartswith(expr, keyword.kwlist):
199.         pass # any python key word
200.     # double under score functions
201.     elif next(r_py_func.finditer(expr), None):
202.         pass
203.     else:
204.         return
205.     # If any of the tests pass, we probably have a security threat
206.     raise PotentialSecurityThreat(orig)
207.
208. def mfix(expr):
209.     """fixes up an expression or equation for evaluation"""
210.     # Save the original expression for accurate error reporting
211.     orig_expr = expr
212.
213.     # Take out all white space
214.     expr = r_white_space.sub("", expr)
215.     if not expr.strip():
216.         return ''
217.
218.     # checks for different kinds of potential security threats
219.     check_security_threat(expr, orig_expr)
220.
221.     #expr = r_sqrt.sub("sqrt(", expr).encode('UTF-8')
222.     # Balance all the different types of parenthesis
223.     expr = balance(expr)
224.     # vector and absoulute operators
225.     #expr = r_mag_mul.sub(lambda m: m.group(1)+m.group(2)+m.group(1)+'*'+m.group(3)+m.group(4)+m.group(3), expr)
226.
227.     # Get vector magnitudes
228.     expr = r_mag.sub(lambda m: m.group(1)+'.magnitude', expr)
229.     # Change |x| to abs(x)
230.     expr = r_abs.sub(lambda m: 'abs('+m.group(1)+')', expr)
231.     # Replace ^ with **, )*(, and remove quotation marks
232.     # This is just to save space and avoid repeating the same code
233.     expr = sreplace(expr, ['^', ')(', '"', "'"], ['**', ')*(', '', ''])
234.     # Replace all different types of brackets with parenthesis
235.     expr = sreplace(expr, '{}[]', '()()')
236.     # Replace 2 -> x with x = 2
237.     expr = r_assign.sub(lambda m: m.group(2)+'='+m.group(1), expr)
238.     # Change a(x) = y into a Python function
239.     expr = r_func_def.sub(repl_func_def, expr)
240.     # Change things like 2x to 2*x
241.     expr = r_terms_mul.sub(lambda m: m.group(1)+"*"+m.group(2), expr)
242.
243.     # Account for the plus or minus symbol
244.     if '\xb1' in expr: # plus or minus symbol
245.         # Check if this is an assignment statement
246.         if '=' in expr.replace('==', ''):
247.             # Get rid of other equal signs
248.             expr = expr.replace('==', '__HOLD__')
249.             if 'lambda' in expr:
250.                 i = expr.index(':', expr.index('lambda'))
251.                 # Find the assignment statement
252.                 i_eq = expr.index('=')
253.                 name, extra, value = expr.split('=')[0], expr[i_eq+1:i+1], expr[i+1:]
254.             else:
255.                 name, value, extra = expr.split('=') + ['']
256.             name += '='
257.             name, value = map(lambda x: x.replace('__HOLD__', '=='), [name, value])
258.         else:
259.             name, extra, value = '', '', expr
260.
261.         # Get both the positive and negative value
262.         expr = name + extra + 'Vector(' + value.replace('\xb1', '+') + ',' + value.replace('\xb1', '-') + ')'
263.
264.     # Look for vectors
265.     for mobj in r_vector.finditer(expr):
266.         vec = mobj.group(1)
267.         if ',' in vec or vec == '()':
268.             expr = expr[:mobj.start()] + 'Vector' + vec + expr[mobj.end():]
269.
270.     # force float
271.     expr = r_int.sub(lambda m: m.group(1)+'.0', expr)
272.     # fix invalid integers created by this
273.     expr = r_invalid_int.sub(lambda m: m.group(1), expr)
274.     #expr = re.sub(r'([[{(])\*([[{(])', lambda m: m.group(1)+m.group(2), expr)
275.     return expr
276.
277. def mcompile(expr):
278.     """Fixes up a math expression and compiles into Python code"""
279.     expr = mfix(expr)
280.     if "=" in expr.replace('==', ''):
281.         sym = "exec"
282.     else:
283.         sym = "eval"
284.     return code.compile_command(expr, symbol=sym)
285.
286. def _calc_fail(signum, frame):
287.     raise CalculationFailure('Calculation Failed!')
288.
289. # Used to reformat results
290. class SimpleRepr:
291.     """Give simple repr'd values for objects"""
292.     def __init__(self, data):
293.         self.data = data
294.
295.     def __repr__(self):
296.         return self.data
297.
298. def find_name(x, scope):
299.     for name, value in scope.iteritems():
300.         if x == value:
301.             return name
302.
303. LAMBDA_NAME = (lambda: None).__name__
304.
305. def reformat_result(iterable, scope={}):
306.     """Change the representation of items in result such as callable or floating point objects"""
307.     new = []
308.     for x in iterable:
309.         if hasattr(x, '__call__'):
310.             name = getattr(x, '__name__')
311.             if name == LAMBDA_NAME:
312.                 name = find_name(x, scope)
313.             new.append(SimpleRepr(name))
314.         elif hasattr(x, '__iter__'):
315.             new.append(reformat_names(x))
316.         elif isinstance(x, complex):
317.             if not x.imag:
318.                 x = float(x.real)
319.             new.append(x)
320.         else:
321.             # float which could be an int in simplest form
322.             if hasattr(x, 'is_integer') and x.is_integer():
323.                 x = int(x)
324.             new.append(x)
325.     return tuple(new)
326.
327. def meval(expr, local={}):
328.     """
329.    Evaluate an expression
330.
331.    Note: You will NEED to set local to global_scope in order to use all the math functions
332.    """
333.     # the expression is either a function or a variable in the local scope
334.     if mfix(expr) in local:
335.         obj = local[mfix(expr)]
336.         # if obj is a function
337.         if hasattr(obj, '__call__'):
338.             return expr
339.         # otherwise, return the object itself
340.         else:
341.             return obj
342.
343.     if MODE_CONST in local and local[MODE_CONST] == DEGREE:
344.         # m.group(2) has parenthesis already around it
345.         expr = r_angle_func.sub(lambda m: m.group(1)+'(radians'+m.group(2)+')', expr)
346.
347.     # The signal code here makes sure that the execution doesn't take too long
348.     if hasattr(signal, "SIGALRM"):
349.         signal.signal(signal.SIGALRM, _calc_fail)
350.         signal.alarm(5)
351.     # Evaluate the result
352.     result = eval(mcompile(expr), local, local)
353.     if hasattr(signal, "SIGALRM"):
354.         signal.alarm(0)
355.     if isinstance(result, complex):
356.         if not result.imag:
357.             result = float(result.real)
358.         else:
359.             # Display complex numbers properly
360.             result = "%s+%si"%(result.real, result.imag)
361.     # float which could be an int in simplest form
362.     if hasattr(result, 'is_integer') and result.is_integer():
363.         result = int(result)
364.     elif hasattr(result, '__iter__'):
365.         if isinstance(result, set):
366.             result = tuple(result)
367.         result = reformat_result(result, local)
368.     return result
369.
370. def get_line():
371.     """Gets a line of input"""
372.     try:
373.         return raw_input('Math> ')
374.     except KeyboardInterrupt:
375.         print 'KeyboardInterrupt'
376.     except:
377.         # Obviously must be in scope to be used
378.         print_exc()
379.     return ""
380.
381. def interact(scope=global_scope.copy()):
382.     """Creates an interactive Math prompt and gives access to default math functions"""
383.     from traceback import print_exc
384.
385.     print "Type 'exit' or 'quit' to end this interpreter"
386.
387.     # Allow for control of the trignometric function modes
388.     if not 'mode' in scope:
389.         make_mode_func(scope)
390.
391.     # Get a single line of input
392.     line = get_line()
393.     while True:
394.         # Let the user quit
395.         if line in ('exit', 'quit'):
396.             break
397.         try:
398.             for expr in line.split(';'):
399.                 if not expr:
400.                     continue
401.                 result = meval(expr, scope)
402.                 if result is not None:
403.                     print result
404.                     # Set the previously found result to the variable _
405.                     scope['_'] = result
406.         except KeyboardInterrupt:
407.             print 'KeyboardInterrupt'
408.         except:
409.             print_exc()
410.         line = get_line()
411.
412. if __name__ == "__main__":
413.     interact()
RAW Paste Data
Top