29

I want to calculate math expression from a string. I have read that the solution to this is to use eval(). But when I try to run the following code:

<?php

$ma ="2+10";
$p = eval($ma);
print $p;

?>

It gives me the following error:

Parse error: syntax error, unexpected $end in C:\xampp\htdocs\eclipseWorkspaceWebDev\MandatoryHandinSite\tester.php(4) : eval()'d code on line 1

Does someone know the solution to this problem.

Langkiller
  • 3,377
  • 13
  • 43
  • 72
  • 5
    You can hack something up using `eval()`, but no one should ever use eval for anything *ever*. [Check this solution](http://stackoverflow.com/a/1015281/1064767). – Sammitch Sep 18 '13 at 19:36
  • okay thanks.. what's so bad about using eval() if I may ask? – Langkiller Sep 18 '13 at 19:45
  • 1
    @user68621: It's *very* insecure. Where's the `$ma` string coming from? User input? What if I sent `rmdir('/var/www');` or something as my input? – gen_Eric Sep 18 '13 at 19:50
  • ahh I see what you mean :) yes $ma is user input. – Langkiller Sep 18 '13 at 20:54
  • 4
    Basically because 90% of the times it is used it's to evaluate code pulled in from external sources which is a security concern. 9.9% of the time it's people approaching a problem wrong. The final 0.1% is a mythical unicorn I have yet to witness whose existence I continue to doubt. Also, the above percentages ignore the *vast* majority of the time where a hacker injects `eval()` code into a vulnerable web page. – Sammitch Sep 18 '13 at 21:37
  • Possible duplicate of [PHP function to evaluate string like "2-1" as arithmetic 2-1=1](http://stackoverflow.com/questions/5057320/php-function-to-evaluate-string-like-2-1-as-arithmetic-2-1-1) – Case Apr 23 '17 at 23:37

10 Answers10

77

While I don't suggest using eval for this (it is not the solution), the problem is that eval expects complete lines of code, not just fragments.

$ma ="2+10";
$p = eval('return '.$ma.';');
print $p;

Should do what you want.


A better solution would be to write a tokenizer/parser for your math expression. Here's a very simple regex-based one to give you an example:

$ma = "2+10";

if(preg_match('/(\d+)(?:\s*)([\+\-\*\/])(?:\s*)(\d+)/', $ma, $matches) !== FALSE){
    $operator = $matches[2];

    switch($operator){
        case '+':
            $p = $matches[1] + $matches[3];
            break;
        case '-':
            $p = $matches[1] - $matches[3];
            break;
        case '*':
            $p = $matches[1] * $matches[3];
            break;
        case '/':
            $p = $matches[1] / $matches[3];
            break;
    }

    echo $p;
}
gen_Eric
  • 223,194
  • 41
  • 299
  • 337
  • 1
    I've added a suggestion for a better solution. It's not the greatest, but I hope it gives an idea into what you need to do. I'm sure you can Google around for a better solution. – gen_Eric Sep 18 '13 at 19:48
  • 1
    This is really good, except in my function where I have 24.5*12, it's taking the first number as 5 instead of 24.5 since it's looking solely for digits – Warren Sergent Feb 02 '16 at 21:43
  • 2
    Replacing the pattern with the following did the trick for me (allows digits, spaces, and a decimal) - note that it will match numbers with spaces in them (i.e. '201.5 + 11 2012 = 212.5', so math wouldn't work entirely correctly without stripping those spaces out (via a `str_replace` around `$matches[1]` and `$matches[3]` or similar). It would be entirely dependent on your usage - this worked for my needs. `/([\d\.\s]+)([\+\-\*\/])([\d\.\s]+)/` – Warren Sergent Feb 03 '16 at 06:28
  • 1
    One step further: `/([\d\.\s]+)([\+\-\*\/])(\-?[\d\.\s]+)/` will allow the second number to be a negative value (in the case of 24.5 * -4 for instance). This would have broken as the second group wouldn't have found the negative number. – Warren Sergent Feb 03 '16 at 06:44
  • How would you advise to tackle `100+01`? I'm using the eval function but I'm kind of confused on how to write a regex to check for numbers such as `011,090,001 etc etc` – halapgos1 Oct 04 '16 at 20:44
  • @halapgos1 The regex in the answer will work as-is. It just looks for a series of things that are digits. Please read up on how regular expressions work; there are plenty of great tutorials. – Nic Apr 16 '17 at 08:07
  • @TeguhSuryoSantoso The "tokenizer" shown isn't very sophisticated. The regex can only parse things like `1+2`. It was given as an example and to be expanded upon when used. – gen_Eric Aug 09 '19 at 14:36
44

Take a look at this..

I use this in an accounting system where you can write math expressions in amount input fields..

Examples

$Cal = new Field_calculate();

$result = $Cal->calculate('5+7'); // 12
$result = $Cal->calculate('(5+9)*5'); // 70
$result = $Cal->calculate('(10.2+0.5*(2-0.4))*2+(2.1*4)'); // 30.4

Code

class Field_calculate {
    const PATTERN = '/(?:\-?\d+(?:\.?\d+)?[\+\-\*\/])+\-?\d+(?:\.?\d+)?/';

    const PARENTHESIS_DEPTH = 10;

    public function calculate($input){
        if(strpos($input, '+') != null || strpos($input, '-') != null || strpos($input, '/') != null || strpos($input, '*') != null){
            //  Remove white spaces and invalid math chars
            $input = str_replace(',', '.', $input);
            $input = preg_replace('[^0-9\.\+\-\*\/\(\)]', '', $input);

            //  Calculate each of the parenthesis from the top
            $i = 0;
            while(strpos($input, '(') || strpos($input, ')')){
                $input = preg_replace_callback('/\(([^\(\)]+)\)/', 'self::callback', $input);

                $i++;
                if($i > self::PARENTHESIS_DEPTH){
                    break;
                }
            }

            //  Calculate the result
            if(preg_match(self::PATTERN, $input, $match)){
                return $this->compute($match[0]);
            }
            // To handle the special case of expressions surrounded by global parenthesis like "(1+1)"
            if(is_numeric($input)){
                return $input;
            }

            return 0;
        }

        return $input;
    }

    private function compute($input){
        $compute = create_function('', 'return '.$input.';');

        return 0 + $compute();
    }

    private function callback($input){
        if(is_numeric($input[1])){
            return $input[1];
        }
        elseif(preg_match(self::PATTERN, $input[1], $match)){
            return $this->compute($match[0]);
        }

        return 0;
    }
}
jptsetung
  • 9,064
  • 3
  • 43
  • 55
clarkk
  • 27,151
  • 72
  • 200
  • 340
  • Your Class is very helpfully! Thank you! But can you expand your class that "(5+2)" not returning 0? – Flo Oct 21 '15 at 07:36
  • can it solve 13/18-1/2 x 40, 13/18 x 40, 14 3/16/ 21 x 40, 16 3/16/ 23 x 40, 16-3/16 / 30 x 60, 9.75 X 21.5/29.5, 1/4 x 5, 1/2 x 6, 7 x 60, 8 Ga. x 4, 22 x 36, 35/64 x 6 – Pushpendra Singh Jul 01 '16 at 10:09
  • I wrote a short test for your expressions @PushpendraSingh If it meets your requirements (e.g. detecting spaces, upper case x, etc) is up to you. https://gist.github.com/DBX12/2e1d622a0fa0937874ac3cf5eeecef51 – DBX12 Mar 06 '17 at 13:02
  • 5
    Since, create_function is depreciated in php 7.2, how would the compute() function be rewritten? I don't want to use eval but this seams to work: private function compute($input) {return 0 + eval('return '.$input.';');} – Brainware Apr 25 '18 at 18:33
  • Add if(is_numeric($input)){return $input;} after if(preg_match(self::PATTERN test if you want it doesn't return 0 for such expressions with global parenthesis "(1+1)" (I modified the code) – jptsetung May 17 '18 at 17:48
  • I have a problem with basic calcul like : $Cal->calculate('0.37+0.31-0.68'), the result must be "0" but the function return : -1.1102230246252E-16, have you an idea of why ? – Florian Richard Feb 04 '22 at 12:58
  • 1
    @FlorianRichard [Because that's the answer.](https://tio.run/##K8go@P/fxr4go4CLKzU5I1/BQM/YXBtIGOoa6JlZWP//DwA) It's because of floating point rounding errors. https://stackoverflow.com/a/22803403/2415524 Numbers are stored in binary. But `0.3` and many others have no finite binary representation, so the computer has to round a little. In the same way, `2/3` has no finite decimal representation, so we might round to `0.6666666667`. – mbomb007 May 24 '22 at 16:29
  • You can fix that issue by multiplying each number by a large integer first, like `100`, then dividing after adding. Or the better way is to use [BC Math or Decimal](https://floating-point-gui.de/languages/php/). – mbomb007 May 24 '22 at 16:34
6

I recently created a PHP package that provides a math_eval helper function. It does exactly what you need, without the need to use the potentially unsafe eval function.

You just pass in the string version of the mathematical expression and it returns the result.

$two   = math_eval('1 + 1');
$three = math_eval('5 - 2');
$ten   = math_eval('2 * 5');
$four  = math_eval('8 / 2');

You can also pass in variables, which will be substituted if needed.

$ten     = math_eval('a + b', ['a' => 7, 'b' => 3]);
$fifteen = math_eval('x * y', ['x' => 3, 'y' => 5]);

Link: https://github.com/langleyfoxall/math_eval

DivineOmega
  • 389
  • 1
  • 5
  • 11
4

Using eval function is very dangerous when you can't control the string argument.

Try Matex for safe Mathematical formulas calculation.

Marcodor
  • 4,578
  • 1
  • 20
  • 24
  • 1
    Which standard you've used to name your variables and methods with first capital letters ? It's not conventional. Friendly advice - change to common standard if you want other people to use your library. – Alex Kalmikov Dec 13 '17 at 10:59
  • @AlexCalm1Kov, oki, will try to adjust to camel, however i don't like it. Currently it is PSR compatible, so should work with auto-loaders. – Marcodor Dec 13 '17 at 11:23
2

Solved!

<?php 
function evalmath($equation)
{
    $result = 0;
    // sanitize imput
    $equation = preg_replace("/[^a-z0-9+\-.*\/()%]/","",$equation);
    // convert alphabet to $variabel 
    $equation = preg_replace("/([a-z])+/i", "\$$0", $equation); 
    // convert percentages to decimal
    $equation = preg_replace("/([+-])([0-9]{1})(%)/","*(1\$1.0\$2)",$equation);
    $equation = preg_replace("/([+-])([0-9]+)(%)/","*(1\$1.\$2)",$equation);
    $equation = preg_replace("/([0-9]{1})(%)/",".0\$1",$equation);
    $equation = preg_replace("/([0-9]+)(%)/",".\$1",$equation);
    if ( $equation != "" ){
        $result = @eval("return " . $equation . ";" );
    }
    if ($result == null) {
        throw new Exception("Unable to calculate equation");
    }
    echo $result;
   // return $equation;
}


$a = 2;
$b = 3;
$c = 5;
$f1 = "a*b+c";

$f1 = str_replace("a", $a, $f1);
$f1 = str_replace("b", $b, $f1);
$f1 = str_replace("c", $c, $f1);

evalmath($f1);
/*if ( $equation != "" ){

    $result = @eval("return " . $equation . ";" );
}
if ($result == null) {

    throw new Exception("Unable to calculate equation");
}
echo $result;*/
?>
perror
  • 7,071
  • 16
  • 58
  • 85
Gowtham Sooryaraj
  • 3,639
  • 3
  • 13
  • 22
1

This method has two major drawbacks:

  • Security, php script is being evaluated by the eval function. This is bad, especially when the user wants to inject malicious code.

  • Complexity

I created this, check it out: Formula Interpreter

How does it work ?

First, create an instance of FormulaInterpreter with the formula and its parameters

$formulaInterpreter = new FormulaInterpreter("x + y", ["x" => 10, "y" => 20]);

Use the execute() method to interpret the formula. It will return the result:

echo $formulaInterpreter->execute();

in a single line

echo (new FormulaInterpreter("x + y", ["x" => 10, "y" => 20]))->execute();

Examples

# Formula: speed = distance / time
$speed = (new FormulaInterpreter("distance/time", ["distance" => 338, "time" => 5]))->execute() ;
echo $speed;


#Venezuela night overtime (ordinary_work_day in hours): (normal_salary * days_in_a_work_month)/ordinary_work_day
$parameters = ["normal_salary" => 21000, "days_in_a_work_month" => 30, "ordinary_work_day" => 8];
$venezuelaLOTTTArt118NightOvertime = (new FormulaInterpreter("(normal_salary/days_in_a_work_month)/ordinary_work_day", $parameters))->execute();
echo $venezuelaLOTTTArt118NightOvertime;


#cicle area
$cicleArea = (new FormulaInterpreter("3.1416*(radio*radio)", ["radio" => 10]))->execute();
echo $cicleArea;

About the formulas

  1. It must contain at least two operands and an operator.
  2. Operands' name could be in upper or lower case.
  3. By now, math functions as sin, cos, pow… are not included. I'm working to include them.
  4. If your formula is not valid, you will get an error message like: Error, your formula (single_variable) is not valid.
  5. Parameters' values must be numeric.

You can improve it if you want to!

Community
  • 1
  • 1
Carlos Espinoza
  • 1,115
  • 11
  • 13
1

Finding a sweetspot between the dangers of eval and the limitless calculation possibilities I suggest checking the input for only numbers, operators and brackets:

if (preg_match('/^[0-9\+\-\*\/\(\)\.]+$/', $mathString)) {
    $value = eval('return
    ' . $mathString . ';');
} else {
    throw new \Exception('Invalid calc() value: ' . $mathString);
}

It's still easy to use yet relatively save. And it can handle any basic math calulation like (10*(1+0,2)) which isn't possible with most of the mentioned solutions here.

ptmr.io
  • 2,115
  • 4
  • 22
  • 34
0

eval Evaluates the given code as PHP. Meaning that it will execute the given paremeter as a PHP piece of code.

To correct your code, use this :

$ma ="print (2+10);";
eval($ma);
S.Thiongane
  • 6,883
  • 3
  • 37
  • 52
0

Using eval function

 protected function getStringArthmeticOperation($value, $deduct)
{
    if($value > 0){
        $operator = '-';
    }else{
        $operator = '+';
    }
    $mathStr = '$value $operator $deduct';
    eval("\$mathStr = \"$mathStr\";");
    $userAvailableUl = eval('return '.$mathStr.';');
    return $userAvailableUl;
}

$this->getStringArthmeticOperation(3, 1); //2
Kaushik shrimali
  • 1,178
  • 8
  • 15
-2

An eval'd expression should end with ";"

Try this :

$ma ="2+10;";
$p = eval($ma);
print $p;

By the way, this is out of scope but the 'eval' function won't return the value of the expression. eval('2+10') won't return 12. If you want it to return 12, you should eval('return 2+10;');

ibtarek
  • 795
  • 4
  • 16