8

I have a question concerning evaluation of math expression within a string. For example my string is following:

my_str='I have 6 * (2 + 3) apples'

I am wondering how to evaluate this string and get the following result:

'I have 30 apples'

Is the any way to do this?

Thanks in advance.

P.S. python's eval function does not help in this case. It raised an error, when trying to evaluate with the eval function.

Óscar López
  • 232,561
  • 37
  • 312
  • 386
Arsen Manukyan
  • 91
  • 1
  • 1
  • 3

8 Answers8

3

Here's my attempt:

>>> import string
>>> s = 'I have 6 * (2+3) apples'
>>> symbols = '^*()/+-'
>>> formula = [(x,s.index(x)) for x in s if x in string.digits+symbols]
>>> result = eval(''.join(x[0] for x in formula), {'__builtins__':None})
>>> s = s[:formula[0][1]] + str(result) + s[formula[-1][1]+1:]
>>> s
'I have 30 apples'

Notes:

This is very simple, it won't deal with complex equations - like those with square root, pi, etc. but I believe its in the spirit of what the question is after. For a really robust answer see the question posted by jeffery_the_wind; but I believe it may be overkill for this simplistic case.

Community
  • 1
  • 1
Burhan Khalid
  • 169,990
  • 18
  • 245
  • 284
2

Sometimes it's better to simplify the question rather than coming up with complicated solutions. You may want to simplify the problem by having your code be provided like this

my_str='I have {6 * (2 + 3)} apples'

This way you can parse it using a simple regex and eval what's inside. Otherwise you're in for a lot of complexity.

joelhoro
  • 890
  • 1
  • 9
  • 20
1

This is a very tricky problem which is probably nearly impossible to solve in general. However, here's a simple way to attack the problem which works with the example input.

*step 1 -- sanitize the input. This is the hardest part to do in general. Basically, you need a way to pull a single math expression out of the string without mangling it. Here a simple regex will work:

sanitized = re.sub(r'[a-zA-Z]','',my_str).strip()

*step 2 -- evaluate using eval:

value = eval(sanitized, {'__builtins__':None})

*step 3 -- back substitute

new_string = my_str.replace(sanitized, str(value))
mgilson
  • 300,191
  • 65
  • 633
  • 696
  • Well, it's not that it cannot be solved in general. It's an ill-defined problem as is (what constitutes an expression to be evaluated and what doesn't?) but once we nail that down, it is easily solved, if you actually bother to parse stuff instead of abusing `eval`. –  Aug 28 '12 at 16:19
  • 1
    @delnan -- if this is `eval` abuse, then what isn't `eval` abuse (at which point, shouldn't it be removed from the language completely?). I think this is a completely fine place to use `eval` given that the problem is constrained enough to parse the expression to be evaluated out of the input string. – mgilson Aug 28 '12 at 17:16
  • I for one think `eval` should not be placed as prominently (i.e. in the global namespace). I know use cases for `compile` and `exec` (and they are significantly different from this, in that they control the input string 100% and it's known what it will do). I have yet to encounter a good use case for `eval` -- when you want to evaluate a math expression, write a shunting yard evaluator or something. If you want to run Python code, use `exec` because it's less restricted. I don't avoid `eval` for correctness, but to not mix up code with data. (Screw the lisp guys who say code is data.) –  Aug 28 '12 at 17:21
  • @delnan -- write your own shunting yard algorithm? Really? Why go through all that work to create and debug something that is already (prominently featured) in the standard library? I understand the argument that you don't know what you'll get out, but that's true to some extent with *any function* in python due to duck-typing and operator overloading. The moral is to put good inputs into your functions and you'll get good stuff back out. Also note that here `'6 * ( 2 + 3 )'` isn't code. It's a string. We're not trying to alter program flow with this or anything... – mgilson Aug 28 '12 at 17:30
  • To back down, my primary point was that many useful definitions of what should be evaluated require some more parsing (cf. the rules on when a `*` in reStructuredText is just a character and not a markup construct). And yes, `6 * ( 2 + 3 )` is a string -- not a Python expression. That's exactly why I object to passing it to `eval`. Besides, SY isn't terribly hard to implement given the details descriptions of it (I did it myself), and easily tested. There's a reasonable limit on what can happen with a function like that, or most other functions. With `eval` etc., the only limit is Python. –  Aug 28 '12 at 17:39
  • Just be careful. We've all been drinking the "eval is evil" koolaid so long, sometimes it's hard to step back and realize that the python devs leave it in there for a reason (mainly that it is useful). Like all powerful tools (e.g. dynamite, table saws ...) you need to handle with care, but "with `eval` the only limit is Python" is only true for a *bare `eval`*. If you pass a globals dictionary, you limit the namespace that `eval` has access to use and (AFAIK), you can even use it safely (as I demonstrated in my answer). But anyway ... – mgilson Aug 28 '12 at 17:48
  • @delnan -- You are right that the most difficult part of this problem is actually figuring out which strings should be treated as expressions and which are just strings. After you have the expression as a string on its own, it gets a lot easier (either using eval, pyparsing or something else ... -- the problem is then well defined at least.) – mgilson Aug 28 '12 at 17:51
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/15916/discussion-between-delnan-and-mgilson) –  Aug 28 '12 at 17:51
1

Thanks to all for your help. Actually my provided example is very simple compared what I have in real task. I read these string from file and sometimes is can have view like this:

my_str='ENC M6_finger_VNCAPa (AA SYZE BY (0.14*2)) < (0.12 + 0.07) OPPOSITE REGION'

Math equation are simple but can occurs many time in one string, and should be evaluated separately.

So I write a sample code, which is able to handle this cases: Maybe it is not such good, but solve the problem:

def eval_math_expressions(filelist):
        for line in filelist:
              if re.match('.*[\-|\+|\*|\/].*',line):
                        lindex=int(filelist.index(line))
                        line_list=line.split()
                        exp=[]
                        for word in line_list:
                                if re.match('^\(+\d+',word) or re.match('^[\)+|\d+|\-|\+|\*|\/]',word):
                                        exp.append(word)
                                else:
                                        ready=' '.join(exp)
                                        if ready:
                                                eval_ready=str(eval(ready))
                                                line_new=line.replace(ready,eval_ready)
                                                line=line_new
                                                filelist[lindex]=line
                                        exp=[]
        return filelist
Óscar López
  • 232,561
  • 37
  • 312
  • 386
Arsen Manukyan
  • 91
  • 1
  • 1
  • 3
1

Use f-strings or slicing.

F-string: f'I have {str(6*(2+3))} apples'

0

For a solution without using eval, here's what I would do. Start by finding all of the mathematical expressions in the string, which I will define as a string that contains spaces, parenthesis, numbers, and operations, then strip out the matches that are all whitespace:

>>> import re
>>> my_str = 'I have 6 * (2 + 3) apples'
>>> exprs = list(re.finditer(r"[\d\.\s\*\+\-\/\(\)]+", my_str))
>>> exprs = [e for e in exprs if len(my_str[e.start():e.end()].strip()) > 0]

Next, evaluate the expressions using the NumericStringParser class from this question, which uses pyparsing:

>>> nsp = NumericStringParser()
>>> results = [nsp.eval(my_str[e.start():e.end()]) for e in exprs]
>>> results
[30.0]

Then, to substitute the results back into the expression, reverse sort the expressions by their starting index and place them back into the original string:

>>> new_str = my_str
>>> for expr, res in sorted(zip(exprs, results), key=lambda t: t[0].start(), reverse=True):
...     new_str = new_str[:expr.start()] + (" %d " % res) + new_str[expr.end():]
... 
>>> new_str
'I have 30 apples'
Community
  • 1
  • 1
jterrace
  • 64,866
  • 22
  • 157
  • 202
0

My option:

>>> import re
>>> def calc(s):
...     val = s.group()
...     if not val.strip(): return val
...     return " %s " % eval(val.strip(), {'__builtins__': None})
>>> re.sub(r"([0-9\ \.\+\*\-\/(\)]+)", calc, "I have 6 * (2 + 3 ) apples")
'I have 30 apples'
Alexey Kachayev
  • 6,106
  • 27
  • 24
-1

[I know this is an old question, but it is worth pointing out new useful solutions as they pop up]

Since python3.6, this capability is now built into the language, coined "f-strings".

See: PEP 498 -- Literal String Interpolation

For example (note the f prefix):

f'I have {6 * (2 + 3)} apples'
=> 'I have 30 apples'
color = 'green'
f'I have {6 * (2 + 3)} {color} apples'
=> 'I have 30 green apples'
shx2
  • 61,779
  • 13
  • 130
  • 153