1

I have a dictionary containing a set of key values available through a web application: I want to process user supplied formulas like: ((value1+value3)/value4)*100

What would be the easiest way to get the formula calculated matching values with ones from the dictionary?

Consider this example:

#!/usr/bin/python
values={'value1':10,'value2':1245,'value3':674365,'value4':65432,'value5':131}
formula=raw_input('Please enter formula:')

If I supply the formula '((value1+value3)/value4)*100' how can I map the value1 etc to map value1 from the dictionary and calculate the result?

Cheers,

jay_t
  • 3,593
  • 4
  • 22
  • 27

3 Answers3

6

eval can be used execute malicious code.

Do you trust your users? If so, you can pass values along as a global dict to be used by eval. Thus, eval can evaluate the user formula directly without any additional string manipulation:

values={'value1':10,'value2':1245,'value3':674365,'value4':65432,'value5':131}
formula=raw_input('Please enter formula:')
values=eval(formula,values)
print(values)

If you do not trust your potential users, you could use pyparsing: The following is Paul McGuire's numeric expression parser, fourFn.py, wrapped in a class for easier use.

utils_parse_numeric.py:

from __future__ import division
from pyparsing import (Literal,CaselessLiteral,Word,Combine,Group,Optional,
                       ZeroOrMore,Forward,nums,alphas,oneOf)
import math
import operator   

class NumericStringParser(object):
    '''
    Most of this code comes from the fourFn.py pyparsing example

    '''
    def pushFirst(self, strg, loc, toks ):
        self.exprStack.append( toks[0] )
    def pushUMinus(self, strg, loc, toks ):
        if toks and toks[0]=='-': 
            self.exprStack.append( 'unary -' )
    def __init__(self):
        """
        expop   :: '^'
        multop  :: '*' | '/'
        addop   :: '+' | '-'
        integer :: ['+' | '-'] '0'..'9'+
        atom    :: PI | E | real | fn '(' expr ')' | '(' expr ')'
        factor  :: atom [ expop factor ]*
        term    :: factor [ multop factor ]*
        expr    :: term [ addop term ]*
        """
        point = Literal( "." )
        e     = CaselessLiteral( "E" )
        fnumber = Combine( Word( "+-"+nums, nums ) + 
                           Optional( point + Optional( Word( nums ) ) ) +
                           Optional( e + Word( "+-"+nums, nums ) ) )
        ident = Word(alphas, alphas+nums+"_$")       
        plus  = Literal( "+" )
        minus = Literal( "-" )
        mult  = Literal( "*" )
        div   = Literal( "/" )
        lpar  = Literal( "(" ).suppress()
        rpar  = Literal( ")" ).suppress()
        addop  = plus | minus
        multop = mult | div
        expop = Literal( "^" )
        pi    = CaselessLiteral( "PI" )
        expr = Forward()
        atom = ((Optional(oneOf("- +")) +
                 (pi|e|fnumber|ident+lpar+expr+rpar).setParseAction(self.pushFirst))
                | Optional(oneOf("- +")) + Group(lpar+expr+rpar)
                ).setParseAction(self.pushUMinus)       
        factor = Forward()
        factor << atom + ZeroOrMore( ( expop + factor ).setParseAction( self.pushFirst ) )
        term = factor + ZeroOrMore( ( multop + factor ).setParseAction( self.pushFirst ) )
        expr << term + ZeroOrMore( ( addop + term ).setParseAction( self.pushFirst ) )
        self.bnf = expr
        epsilon = 1e-12
        self.opn = { "+" : operator.add,
                "-" : operator.sub,
                "*" : operator.mul,
                "/" : operator.truediv,
                "^" : operator.pow }
        self.fn  = { "sin" : math.sin,
                "cos" : math.cos,
                "tan" : math.tan,
                "abs" : abs,
                "trunc" : lambda a: int(a),
                "round" : round,
                "sgn" : lambda a: abs(a)>epsilon and cmp(a,0) or 0}
    def evaluateStack(self, s ):
        op = s.pop()
        if op == 'unary -':
            return -self.evaluateStack( s )
        if op in "+-*/^":
            op2 = self.evaluateStack( s )
            op1 = self.evaluateStack( s )
            return self.opn[op]( op1, op2 )
        elif op == "PI":
            return math.pi # 3.1415926535
        elif op == "E":
            return math.e  # 2.718281828
        elif op in self.fn:
            return self.fn[op]( self.evaluateStack( s ) )
        elif op[0].isalpha():
            return 0
        else:
            return float( op )
    def eval(self,num_string,parseAll=True):
        self.exprStack=[]
        results=self.bnf.parseString(num_string,parseAll)
        val=self.evaluateStack( self.exprStack[:] )
        return val

Then your script could do something like this:

import re
import utils_parse_numeric as upn

my_dict={
    'number1':54,
    'number2':1234,
    'number3':778,
    'number25':2109}

This uses the re module to substitute my_dict["numberXXX"] for 'numberXXX':

def callback(match):
    num=match.group(1)
    key='number{0}'.format(num)
    val=my_dict[key]
    return str(val)

astr='((number1+number3)/number2)*100'
astr=re.sub('number(\d+)',callback,astr)

and here's how the NumericStringParser can be used to safely evaluate numeric expressions:

nsp=upn.NumericStringParser()
result=nsp.eval(astr)
print(result)

This is much safer than using eval. All invalid expressions will raise a pyparsing.ParseException.

unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
0

After validating both the formula and the numbers value (f.e. via a regexp) you can do something like:

arr = {'num1':4, 'num2':5, 'num3':7}
formula = '(num1+num2)*num3'

for key, val in arr.items():
    formula = formula.replace(key, str(val))

res = eval(formula)
print res
Iacopo
  • 4,224
  • 1
  • 23
  • 25
  • That's interesting ... 2 parts i was missing, the replace and eval ... I will look into this but it looks like an option indeed. – jay_t Jun 11 '10 at 12:14
  • 1
    If the user inputs the formula, be sure to validate against arbitrary code execution. If you want just arithmetic, just check that the forumla does not contain any character after the substitution. – Iacopo Jun 11 '10 at 12:15
  • 1
    Make sure you either sanitise the input, or only read from a trusted input. eval() will run anything, even if it means against your machine. – Xavier Ho Jun 11 '10 at 12:36
0

Thanks for all the input. I personally found that lacopo's answer suites my situation best.

Here's a rough idea of the solution:

import sys

values={'one':10,'two':1245,'three':674365,'four':65432,'five':131}
print str(values)

formula=raw_input('Please enter formula:')

for key, val in values.items():
        formula = formula.replace(key, str(val))

whitelist=[ '+','-','/','*','^','(',')' ]

to_evaluate=re.findall('\D',formula)
to_evaluate=list(set(to_evaluate))

for element in to_evaluate:
        if not element in whitelist:
                print "Formula contains an invalid character: "+str(element)
                sys.exit(1)


print eval(formula)
jay_t
  • 3,593
  • 4
  • 22
  • 27