Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # -*- 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'(?<!\w)(\([^\)]*\))')
- # Finds angle functions
- r_angle_func = re.compile(r'(a?sin|cos|tan{1}h?)(\([^\)]+\))')
- # Supports mathematical abs notation: e.g. |x|
- r_abs = re.compile(r'\|([^*/]+)\|')
- # Finds the magnitude of a vector ||v||
- r_mag = re.compile(r'\|\|([^*/]+)\|\|')
- #r_mag_mul = re.compile(r'(\|{1,2})(.+\|)\1(\|{1,2})(.+\|)\3')
- # Finds integers
- r_int = re.compile(r'(?<!\.)(\d+)(?!\.)')
- # Finds invalid integers (syntax error)
- r_invalid_int = re.compile(r'(\d+\.\d+)\.0')
- # Finds decimal numbers
- r_deg = re.compile(r"[+-]?((\d+(\.\d*)?)|\.\d+)([eE][+-]?[0-9]+)?\xb0")
- # Looks for words (variables, function names, etc)
- r_word = re.compile(r"\w+")
- # Looks for the square root symbol
- r_sqrt = re.compile(u"\u221a"+r"(\()?")
- # constants
- DEFAULT_VARIABLE = "x"
- # calculator modes
- RADIAN = 'RADIAN'
- DEGREE = 'DEGREE'
- MODE_CONST = 'CALC_MODE' # used for detecting modes
- DEFAULT_MODE = RADIAN
- # errors
- class Error(Exception): pass # base exception for this module
- class PotentialSecurityThreat(Error): pass
- class CalculationFailure(Error): pass
- # create a global scope which will be used during the evaluation of the expression
- global_scope = {
- '__builtins__': {},
- MODE_CONST: DEFAULT_MODE,
- 'RADIAN': RADIAN,
- 'DEGREE': DEGREE
- }
- # make sure the user has access to math and cmath variables
- for mod in [math, cmath]:
- global_scope.update(vars(mod))
- # things that are needed from the builtin functions
- builtins_needed = {
- 'abs': abs,
- 'int': int,
- 'float': float,
- 'round': round,
- }
- # add specific built in functions (and nothing else)
- global_scope.update(builtins_needed)
- # load extension modules
- for name in 'Vector fib'.split():
- try:
- global_scope[name] = value = getattr(__import__(name.lower()), name)
- exec "global %s"%name
- exec "%s = getattr(__import__(%r.lower()), %r)"%(name, name, name)
- except:
- pass
- def vrange(start, stop=None, step=1):
- """Vector of range(start, stop, step)"""
- if stop is None:
- stop = start
- start = 0
- return Vector(range(int(start), int(stop), int(step)))
- # add this to the global scope so the user has access
- global_scope["vrange"] = vrange
- def dextend(*args):
- """Update the first argument with all the others"""
- ext = args[0]
- for d in args[1:]:
- ext.update(d)
- return ext
- def make_mode_func(scope):
- """Make a function to define the calculator mode"""
- def _mode(mode=None):
- if mode and mode in [RADIAN, DEGREE]:
- scope[MODE_CONST] = mode
- return scope[MODE_CONST]
- scope['mode'] = _mode
- def sstartswith(string, prefixes):
- """Return True if string starts with any of the prefixes"""
- for prefix in prefixes:
- if string.startswith(prefix):
- return True
- def sreplace(s, old, new):
- """Basically goes through old and replaces old[i] with the corresponding new[i]"""
- for x, y in izip(old, new):
- s = s.replace(x, y)
- return s
- BRACKETS = dict(izip('[{(', ']})'))
- def balance(s):
- """
- Balance the parentheses within a string
- sin(cos(tan(x) --> 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()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement