# -*- coding: cp1252 -*- """ Mathematical Expression Evaluator Author: Sunjay Varma Website: www.sunjay.ca Supports a wide array of syntaxes and expressions. * For complex numbers, use complex() * use mode(RADIANS) or mode(DEGREES) to change trig mode * You can define functions using 'f(x) =' notation (multivariable support e.g. 'abc(x, y, z) =') * You can define variables with the 2 -> x notation (or more generally, expression -> expression) * You can multiply using 2x and (2)(-4) notation * Vectors are created for you and used as lists of numbers (YOU MUST HAVE THE 'Vector' MODULE.) * 'fib' is an optional module that generates fibonacci numbers (use the fib function) * The evaluator will balance parenthesis for you * You can use |variable_name| for abs(variable_name) * Use ||v|| to find the magnitude of Vector 'v' * You can use ^ for exponents: e.g. f(x) = x^2 is the same as f(x) = x**2 * Technically, this supports ± (plus or minus) and square root symbols as well You MUST set local to 'global_scope' when using meval if you want to have all the math functions! It should stop you from trying any real Python expressions...let me know if you find any bugs! General usage: >>> meval('sin(pi/pi-1)', global_scope) 0 >>> meval('2x', {'x': 8}) 16 >>> interact() Type 'exit' or 'quit' to end this interpreter Math> 2 + 2 4 Math> 2(4) 8 Math> 2(4)^2 32 Math> exit """ from __future__ import division import cmath, math, re, code from itertools import imap, izip import keyword import signal # regular expressions #r_term_parts = re.compile(r'(\d*)([^\^]+)?') #r_plus_minus = re.compile(r'\+\s*\-') # Used to get rid of all white space r_white_space = re.compile(r'\s+') # Math functions, not python functions: e.g. f(x) = y, abc(x, y, z) = q r_func_def = re.compile(r'([a-zA-Z_]+)\(([^\)]+)\)\=') # Function used for r_func_def to turn math functions into Python ones repl_func_def = lambda m: m.group(1)+' = lambda ' + m.group(2) + ': ' # In math, we can do lots of fancy things with multiplication that we can't do in Python # This translates most math multiplication notation into Python r_terms_mul = re.compile(r''' # fix things like 2x and 2(4) (\d+ # number | # or [^a-zA-Z\*\/+\-\(\,\=\:><\|!.; \t\n\r\f\v\xb1_] # anything not a letter or operator ) ([a-zA-Z_\(]+) # either parenthesis or a letter ''', re.VERBOSE) # Catches functions with '__' around them r_py_func = re.compile(r'__([^_]+)__') # Supports assignment through 2 -> x r_assign = re.compile(r'(.+)(?=\-\>)\-\>(.+)') # Finds vectors r_vector = re.compile(r'(? sin(cos(tan(x))) """ openedb = BRACKETS.keys() closedb = BRACKETS.values() if [s.count(x) for x in openedb] == [s.count(x) for x in closedb]: return s # balanced open_br = [] for c in s: if c in openedb: open_br.append(c) elif c in closedb: if not open_br or not BRACKETS[open_br[-1]] == c: raise SyntaxError('Invalid Syntax!') # too many closing brackets open_br.pop() for br in open_br: s += BRACKETS[br] return s def check_security_threat(expr, orig): """Looks for potential security threats by seeking out python keywords and underscores""" if sstartswith(expr, keyword.kwlist): pass # any python key word # double under score functions elif next(r_py_func.finditer(expr), None): pass else: return # If any of the tests pass, we probably have a security threat raise PotentialSecurityThreat(orig) def mfix(expr): """fixes up an expression or equation for evaluation""" # Save the original expression for accurate error reporting orig_expr = expr # Take out all white space expr = r_white_space.sub("", expr) if not expr.strip(): return '' # checks for different kinds of potential security threats check_security_threat(expr, orig_expr) #expr = r_sqrt.sub("sqrt(", expr).encode('UTF-8') # Balance all the different types of parenthesis expr = balance(expr) # vector and absoulute operators #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) # Get vector magnitudes expr = r_mag.sub(lambda m: m.group(1)+'.magnitude', expr) # Change |x| to abs(x) expr = r_abs.sub(lambda m: 'abs('+m.group(1)+')', expr) # Replace ^ with **, )*(, and remove quotation marks # This is just to save space and avoid repeating the same code expr = sreplace(expr, ['^', ')(', '"', "'"], ['**', ')*(', '', '']) # Replace all different types of brackets with parenthesis expr = sreplace(expr, '{}[]', '()()') # Replace 2 -> x with x = 2 expr = r_assign.sub(lambda m: m.group(2)+'='+m.group(1), expr) # Change a(x) = y into a Python function expr = r_func_def.sub(repl_func_def, expr) # Change things like 2x to 2*x expr = r_terms_mul.sub(lambda m: m.group(1)+"*"+m.group(2), expr) # Account for the plus or minus symbol if '\xb1' in expr: # plus or minus symbol # Check if this is an assignment statement if '=' in expr.replace('==', ''): # Get rid of other equal signs expr = expr.replace('==', '__HOLD__') if 'lambda' in expr: i = expr.index(':', expr.index('lambda')) # Find the assignment statement i_eq = expr.index('=') name, extra, value = expr.split('=')[0], expr[i_eq+1:i+1], expr[i+1:] else: name, value, extra = expr.split('=') + [''] name += '=' name, value = map(lambda x: x.replace('__HOLD__', '=='), [name, value]) else: name, extra, value = '', '', expr # Get both the positive and negative value expr = name + extra + 'Vector(' + value.replace('\xb1', '+') + ',' + value.replace('\xb1', '-') + ')' # Look for vectors for mobj in r_vector.finditer(expr): vec = mobj.group(1) if ',' in vec or vec == '()': expr = expr[:mobj.start()] + 'Vector' + vec + expr[mobj.end():] # force float expr = r_int.sub(lambda m: m.group(1)+'.0', expr) # fix invalid integers created by this expr = r_invalid_int.sub(lambda m: m.group(1), expr) #expr = re.sub(r'([[{(])\*([[{(])', lambda m: m.group(1)+m.group(2), expr) return expr def mcompile(expr): """Fixes up a math expression and compiles into Python code""" expr = mfix(expr) if "=" in expr.replace('==', ''): sym = "exec" else: sym = "eval" return code.compile_command(expr, symbol=sym) def _calc_fail(signum, frame): raise CalculationFailure('Calculation Failed!') # Used to reformat results class SimpleRepr: """Give simple repr'd values for objects""" def __init__(self, data): self.data = data def __repr__(self): return self.data def find_name(x, scope): for name, value in scope.iteritems(): if x == value: return name LAMBDA_NAME = (lambda: None).__name__ def reformat_result(iterable, scope={}): """Change the representation of items in result such as callable or floating point objects""" new = [] for x in iterable: if hasattr(x, '__call__'): name = getattr(x, '__name__') if name == LAMBDA_NAME: name = find_name(x, scope) new.append(SimpleRepr(name)) elif hasattr(x, '__iter__'): new.append(reformat_names(x)) elif isinstance(x, complex): if not x.imag: x = float(x.real) new.append(x) else: # float which could be an int in simplest form if hasattr(x, 'is_integer') and x.is_integer(): x = int(x) new.append(x) return tuple(new) def meval(expr, local={}): """ Evaluate an expression Note: You will NEED to set local to global_scope in order to use all the math functions """ # the expression is either a function or a variable in the local scope if mfix(expr) in local: obj = local[mfix(expr)] # if obj is a function if hasattr(obj, '__call__'): return expr # otherwise, return the object itself else: return obj if MODE_CONST in local and local[MODE_CONST] == DEGREE: # m.group(2) has parenthesis already around it expr = r_angle_func.sub(lambda m: m.group(1)+'(radians'+m.group(2)+')', expr) # The signal code here makes sure that the execution doesn't take too long if hasattr(signal, "SIGALRM"): signal.signal(signal.SIGALRM, _calc_fail) signal.alarm(5) # Evaluate the result result = eval(mcompile(expr), local, local) if hasattr(signal, "SIGALRM"): signal.alarm(0) if isinstance(result, complex): if not result.imag: result = float(result.real) else: # Display complex numbers properly result = "%s+%si"%(result.real, result.imag) # float which could be an int in simplest form if hasattr(result, 'is_integer') and result.is_integer(): result = int(result) elif hasattr(result, '__iter__'): if isinstance(result, set): result = tuple(result) result = reformat_result(result, local) return result def get_line(): """Gets a line of input""" try: return raw_input('Math> ') except KeyboardInterrupt: print 'KeyboardInterrupt' except: # Obviously must be in scope to be used print_exc() return "" def interact(scope=global_scope.copy()): """Creates an interactive Math prompt and gives access to default math functions""" from traceback import print_exc print "Type 'exit' or 'quit' to end this interpreter" # Allow for control of the trignometric function modes if not 'mode' in scope: make_mode_func(scope) # Get a single line of input line = get_line() while True: # Let the user quit if line in ('exit', 'quit'): break try: for expr in line.split(';'): if not expr: continue result = meval(expr, scope) if result is not None: print result # Set the previously found result to the variable _ scope['_'] = result except KeyboardInterrupt: print 'KeyboardInterrupt' except: print_exc() line = get_line() if __name__ == "__main__": interact()