dolibarr 24.0.0-beta
evalmath.class.php
Go to the documentation of this file.
1<?php
2/* ================================================================================
3 *
4 * EvalMath - PHP Class to safely evaluate math expressions
5 * Copyright (C) 2005 Miles Kaufmann <http://www.twmagic.com/>
6 *
7 * ================================================================================
8 *
9 * NAME
10 * EvalMath - safely evaluate math expressions
11 *
12 * SYNOPSIS
13 * include('evalmath.class.php');
14 * $m = new EvalMath;
15 * // basic evaluation:
16 * $result = $m->evaluate('2+2');
17 * // supports: order of operation; parentheses; negation; built-in functions
18 * $result = $m->evaluate('-8(5/2)^2*(1-sqrt(4))-8');
19 * // create your own variables
20 * $m->evaluate('a = e^(ln(pi))');
21 * // or functions
22 * $m->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1');
23 * // and then use them
24 * $result = $m->evaluate('3*f(42,a)');
25 *
26 * DESCRIPTION
27 * Use the EvalMath class when you want to evaluate mathematical expressions
28 * from untrusted sources. You can define your own variables and functions,
29 * which are stored in the object. Try it, it's fun!
30 *
31 * METHODS
32 * $m->evaluate($expr)
33 * Evaluates the expression and returns the result. If an error occurs,
34 * prints a warning and returns false. If $expr is a function assignment,
35 * returns true on success.
36 *
37 * $m->e($expr)
38 * A synonym for $m->evaluate().
39 *
40 * $m->vars()
41 * Returns an associative array of all user-defined variables and values.
42 *
43 * $m->funcs()
44 * Returns an array of all user-defined functions.
45 *
46 * PARAMETERS
47 * $m->suppress_errors
48 * Set to true to turn off warnings when evaluating expressions
49 *
50 * $m->last_error
51 * If the last evaluation failed, contains a string describing the error.
52 * (Useful when suppress_errors is on).
53 *
54 * $m->last_error_code
55 * If the last evaluation failed, 2 element array with numeric code and extra info
56 *
57 * AUTHOR INFORMATION
58 * Copyright 2005, Miles Kaufmann.
59 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
60 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
61 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
62 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
63 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
64 * Copyright (C) 2025 MDW <mdeweerd@users.noreply.github.com>
65 * Copyright (C) 2025 MDW <mdeweerd@users.noreply.github.com>
66 * Copyright (C) 2025 MDW <mdeweerd@users.noreply.github.com>
67 * Copyright (C) 2025 MDW <mdeweerd@users.noreply.github.com>
68 *
69 * LICENSE
70 * Redistribution and use in source and binary forms, with or without
71 * modification, are permitted provided that the following conditions are
72 * met:
73 *
74 * 1 Redistributions of source code must retain the above copyright
75 * notice, this list of conditions and the following disclaimer.
76 * 2. Redistributions in binary form must reproduce the above copyright
77 * notice, this list of conditions and the following disclaimer in the
78 * documentation and/or other materials provided with the distribution.
79 * 3. The name of the author may not be used to endorse or promote
80 * products derived from this software without specific prior written
81 * permission.
82 *
83 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
84 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
85 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
86 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
87 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
88 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
89 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
90 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
91 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
92 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
93 * POSSIBILITY OF SUCH DAMAGE.
94 */
95
106{
110 public $suppress_errors = false;
111
115 public $last_error = null;
116
120 public $last_error_code = null;
121
125 public $v = array('e' => 2.71, 'pi' => 3.14159);
126
130 public $f = array();
131
135 public $vb = array('e', 'pi');
136
140 public $fb = array(
141 'sin', 'sinh', 'arcsin', 'asin', 'arcsinh', 'asinh', 'cos', 'cosh', 'arccos', 'acos', 'arccosh', 'acosh', 'tan', 'tanh', 'arctan', 'atan', 'arctanh', 'atanh', 'sqrt', 'abs', 'ln', 'log', 'intval', 'ceil',
142 );
143
147 public function __construct()
148 {
149 // make the variables a little more accurate
150 $this->v['pi'] = pi();
151 $this->v['e'] = exp(1);
152 }
153
160 public function e($expr)
161 {
162 return $this->evaluate($expr);
163 }
164
171 public function evaluate($expr)
172 {
173 if (empty($expr)) {
174 return false;
175 }
176
177 $this->last_error = null;
178 $this->last_error_code = null;
179 $expr = trim($expr);
180 if (substr($expr, - 1, 1) == ';') {
181 $expr = substr($expr, 0, strlen($expr) - 1); // strip semicolons at the end
182 }
183 // ===============
184 // is it a variable assignment?
185 $matches = array();
186 if (preg_match('/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches)) {
187 if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant
188 return $this->trigger(1, "cannot assign to constant '$matches[1]'", $matches[1]);
189 }
190 if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) {
191 return false; // get the result and make sure it's good
192 }
193 $this->v[$matches[1]] = $tmp; // if so, stick it in the variable array
194 return $this->v[$matches[1]]; // and return the resulting value
195 // ===============
196 // is it a function assignment?
197 } elseif (preg_match('/^\s*([a-z]\w*)\s*\‍(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\‍)\s*=\s*(.+)$/', $expr, $matches)) {
198 $fnn = $matches[1]; // get the function name
199 if (in_array($matches[1], $this->fb)) { // make sure it isn't built in
200 return $this->trigger(2, "cannot redefine built-in function '$matches[1]()'", $matches[1]);
201 }
202 $args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments
203 if (($stack = $this->nfx($matches[3])) === false) {
204 return false; // see if it can be converted to postfix
205 }
206 $nbstack = count($stack);
207 for ($i = 0; $i < $nbstack; $i++) { // freeze the state of the non-argument variables
208 $token = $stack[$i];
209 if (preg_match('/^[a-z]\w*$/', $token) and !in_array($token, $args)) {
210 if (array_key_exists($token, $this->v)) {
211 $stack[$i] = $this->v[$token];
212 } else {
213 return $this->trigger(3, "undefined variable '$token' in function definition", $token);
214 }
215 }
216 }
217 $this->f[$fnn] = array('args' => $args, 'func' => $stack);
218 return true;
219 // ===============
220 } else {
221 return $this->pfx($this->nfx($expr)); // straight up evaluation, woo
222 }
223 }
224
230 public function vars()
231 {
232 $output = $this->v;
233 unset($output['pi']);
234 unset($output['e']);
235 return $output;
236 }
237
243 private function funcs() // @phpstan-ignore-line
244 {
245 $output = array();
246 foreach ($this->f as $fnn => $dat) {
247 $output[] = $fnn.'('.implode(',', $dat['args']).')';
248 }
249 return $output;
250 }
251
252 // ===================== HERE BE INTERNAL METHODS ====================\\
253
260 private function nfx($expr)
261 {
262 $index = 0;
263 $stack = new EvalMathStack();
264 $output = array(); // postfix form of expression, to be passed to pfx()
265 $expr = trim(strtolower($expr));
266
267 $ops = array('+', '-', '*', '/', '^', '_');
268 $ops_r = array('+' => 0, '-' => 0, '*' => 0, '/' => 0, '^' => 1); // right-associative operator?
269 $ops_p = array('+' => 0, '-' => 0, '*' => 1, '/' => 1, '_' => 1, '^' => 2); // operator precedence
270
271 $expecting_op = false; // we use this in syntax-checking the expression
272 // and determining when a - is a negation
273
274 $matches = array();
275 if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good
276 return $this->trigger(4, "illegal character '".$matches[0]."'", $matches[0]);
277 }
278
279 while (1) { // 1 Infinite Loop ;)
280 $op = substr($expr, $index, 1); // get the first character at the current index
281 // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
282 $match = array();
283 $ex = preg_match('/^([a-z]\w*\‍(?|\d+(?:\.\d*)?|\.\d+|\‍()/', substr($expr, $index), $match);
284 // ===============
285 if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
286 $stack->push('_'); // put a negation on the stack
287 $index++;
288 } elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack
289 return $this->trigger(4, "illegal character '_'", "_"); // but not in the input expression
290 // ===============
291 } elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack?
292 if ($ex) { // are we expecting an operator but have a number/variable/function/opening parenthesis?
293 $op = '*';
294 $index--; // it's an implicit multiplication
295 }
296 // heart of the algorithm:
297 while ($stack->count > 0 and ($o2 = $stack->last()) and in_array($o2, $ops) and ($ops_r[$op] ? $ops_p[$op] < $ops_p[$o2] : $ops_p[$op] <= $ops_p[$o2])) {
298 $output[] = $stack->pop(); // pop stuff off the stack into the output
299 }
300 // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
301 $stack->push($op); // finally put OUR operator onto the stack
302 $index++;
303 $expecting_op = false;
304 // ===============
305 } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
306 while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last (
307 if (is_null($o2)) {
308 return $this->trigger(5, "unexpected ')'", ")");
309 } else {
310 $output[] = $o2;
311 }
312 }
313 if (preg_match("/^([a-z]\w*)\‍($/", $stack->last(2), $matches)) { // did we just close a function?
314 $fnn = $matches[1]; // get the function name
315 $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
316 $output[] = $stack->pop(); // pop the function and push onto the output
317 if (in_array($fnn, $this->fb)) { // check the argument count
318 if ($arg_count > 1) {
319 return $this->trigger(6, "wrong number of arguments ($arg_count given, 1 expected)", array($arg_count, 1));
320 }
321 } elseif (array_key_exists($fnn, $this->f)) {
322 if ($arg_count != count($this->f[$fnn]['args'])) {
323 return $this->trigger(6, "wrong number of arguments ($arg_count given, ".count($this->f[$fnn]['args'])." expected)", array($arg_count, count($this->f[$fnn]['args'])));
324 }
325 } else { // did we somehow push a non-function on the stack? this should never happen
326 return $this->trigger(7, "internal error");
327 }
328 }
329 $index++;
330 // ===============
331 } elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
332 while (($o2 = $stack->pop()) != '(') {
333 if (is_null($o2)) {
334 return $this->trigger(5, "unexpected ','", ","); // oops, never had a (
335 } else {
336 $output[] = $o2; // pop the argument expression stuff and push onto the output
337 }
338 }
339 // make sure there was a function
340 if (!preg_match("/^([a-z]\w*)\‍($/", $stack->last(2), $matches)) {
341 return $this->trigger(5, "unexpected ','", ",");
342 }
343 $stack->push((string) ($stack->pop() + 1)); // increment the argument count
344 $stack->push('('); // put the ( back on, we'll need to pop back to it again
345 $index++;
346 $expecting_op = false;
347 // ===============
348 } elseif ($op == '(' and !$expecting_op) {
349 $stack->push('('); // that was easy
350 $index++;
351 $allow_neg = true;
352 // ===============
353 } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
354 $expecting_op = true;
355 $val = $match[1];
356 if (preg_match("/^([a-z]\w*)\‍($/", $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
357 if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f)) { // it's a func
358 $stack->push($val);
359 $stack->push('1');
360 $stack->push('(');
361 $expecting_op = false;
362 } else { // it's a var w/ implicit multiplication
363 $val = $matches[1];
364 $output[] = $val;
365 }
366 } else { // it's a plain old var or num
367 $output[] = $val;
368 }
369 $index += strlen($val);
370 // ===============
371 } elseif ($op == ')') { // miscellaneous error checking
372 return $this->trigger(5, "unexpected ')'", ")");
373 } elseif (in_array($op, $ops) and !$expecting_op) {
374 return $this->trigger(8, "unexpected operator '$op'", $op);
375 } else { // I don't even want to know what you did to get here
376 return $this->trigger(9, "an unexpected error occurred");
377 }
378 if ($index == strlen($expr)) {
379 if (in_array($op, $ops)) { // did we end with an operator? bad.
380 return $this->trigger(10, "operator '$op' lacks operand", $op);
381 } else {
382 break;
383 }
384 }
385 while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace
386 $index++; // into implicit multiplication if no operator is there)
387 }
388 }
389 while (!is_null($ope = $stack->pop())) { // pop everything off the stack and push onto output
390 if ($ope == '(') {
391 return $this->trigger(11, "expecting ')'", ")"); // if there are (s on the stack, ()s were unbalanced
392 }
393 $output[] = $ope;
394 }
395
396 return $output;
397 }
398
406 private function pfx($tokens, $vars = array())
407 {
408 $stack = new EvalMathStack();
409
410 foreach ($tokens as $token) { // nice and easy
411 // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
412 $matches = array();
413 if (in_array($token, array('+', '-', '*', '/', '^'))) {
414 if (is_null($op2 = $stack->pop())) {
415 return $this->trigger(12, "internal error");
416 }
417 if (is_null($op1 = $stack->pop())) {
418 return $this->trigger(13, "internal error");
419 }
420 switch ($token) {
421 case '+':
422 $stack->push((string) ($op1 + $op2));
423 break;
424 case '-':
425 $stack->push((string) ($op1 - $op2));
426 break;
427 case '*':
428 $stack->push((string) ($op1 * $op2));
429 break;
430 case '/':
431 if ($op2 == 0) {
432 return $this->trigger(14, "division by zero");
433 }
434 $stack->push((string) ($op1 / $op2));
435 break;
436 case '^':
437 $stack->push((string) pow($op1, $op2));
438 break;
439 }
440 // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
441 } elseif ($token == "_") {
442 $stack->push((string) (-1 * $stack->pop()));
443 // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
444 } elseif (preg_match("/^([a-z]\w*)\‍($/", $token, $matches)) { // it's a function!
445 $fnn = $matches[1];
446 if (in_array($fnn, $this->fb)) { // built-in function:
447 if (is_null($op1 = $stack->pop())) {
448 return $this->trigger(15, "internal error");
449 }
450 $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
451 if ($fnn == 'ln') {
452 $fnn = 'log';
453 }
454 // @phan-suppress-next-line PhanPluginUnsafeEval
455 eval('$stack->push('.$fnn.'($op1));'); // perfectly safe eval()
456 } elseif (array_key_exists($fnn, $this->f)) { // user function
457 // get args
458 $args = array();
459 for ($i = count($this->f[$fnn]['args']) - 1; $i >= 0; $i--) {
460 if (is_null($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) {
461 return $this->trigger(16, "internal error");
462 }
463 }
464 $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
465 }
466 // if the token is a number or variable, push it on the stack
467 } else {
468 if (is_numeric($token)) {
469 $stack->push($token);
470 } elseif (array_key_exists($token, $this->v)) {
471 $stack->push($this->v[$token]);
472 } elseif (array_key_exists($token, $vars)) {
473 $stack->push($vars[$token]);
474 } else {
475 return $this->trigger(17, "undefined variable '$token'", $token);
476 }
477 }
478 }
479 // when we're out of tokens, the stack should have a single element, the final result
480 if ($stack->count != 1) {
481 return $this->trigger(18, "internal error");
482 }
483 return $stack->pop();
484 }
485
494 public function trigger($code, $msg, $info = null)
495 {
496 $this->last_error = $msg;
497 $this->last_error_code = array($code, $info);
498 if (!$this->suppress_errors) {
499 trigger_error($msg, E_USER_WARNING);
500 }
501 return false;
502 }
503}
504
509{
511 public $stack = array();
512
514 public $count = 0;
515
522 public function push($val)
523 {
524 $this->stack[$this->count] = $val;
525 $this->count++;
526 }
527
533 public function pop()
534 {
535 if ($this->count > 0) {
536 $this->count--;
537 return $this->stack[$this->count];
538 }
539 return null;
540 }
541
548 public function last($n = 1)
549 {
550 if (isset($this->stack[$this->count - $n])) {
551 return $this->stack[$this->count - $n];
552 }
553
554 return '';
555 }
556}
Class EvalMath.
vars()
Function vars.
trigger($code, $msg, $info=null)
trigger an error, but nicely, if need be
evaluate($expr)
Evaluate.
funcs()
Function funcs.
e($expr)
Evaluate.
__construct()
Constructor.
nfx($expr)
Convert infix to postfix notation.
pfx($tokens, $vars=array())
Evaluate postfix notation.
Class for internal use.