Advertisement
piguy123

equation_evaluator.py

Mar 26th, 2013
374
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.14 KB | None | 0 0
  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
  93. RADIAN = 'RADIAN'
  94. DEGREE = 'DEGREE'
  95. MODE_CONST = 'CALC_MODE' # used for detecting modes
  96. DEFAULT_MODE = RADIAN
  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,
  107.     'RADIAN': RADIAN,
  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.  
  126. # load extension modules
  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()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement