ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.EvalMath.php
Go to the documentation of this file.
1<?php
2
3/*
4================================================================================
5
6EvalMath - PHP Class to safely evaluate math expressions
7Copyright (C) 2005 Miles Kaufmann <http://www.twmagic.com/>
8
9================================================================================
10
11NAME
12 EvalMath - safely evaluate math expressions
13
14SYNOPSIS
15 <?
16 include('evalmath.class.php');
17 $m = new EvalMath;
18 // basic evaluation:
19 $result = $m->evaluate('2+2');
20 // supports: order of operation; parentheses; negation; built-in functions
21 $result = $m->evaluate('-8(5/2)^2*(1-sqrt(4))-8');
22 // create your own variables
23 $m->evaluate('a = e^(ln(pi))');
24 // or functions
25 $m->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1');
26 // and then use them
27 $result = $m->evaluate('3*f(42,a)');
28 ?>
29
30DESCRIPTION
31 Use the EvalMath class when you want to evaluate mathematical expressions
32 from untrusted sources. You can define your own variables and functions,
33 which are stored in the object. Try it, it's fun!
34
35METHODS
36 $m->evalute($expr)
37 Evaluates the expression and returns the result. If an error occurs,
38 prints a warning and returns false. If $expr is a function assignment,
39 returns true on success.
40
41 $m->e($expr)
42 A synonym for $m->evaluate().
43
44 $m->vars()
45 Returns an associative array of all user-defined variables and values.
46
47 $m->funcs()
48 Returns an array of all user-defined functions.
49
50PARAMETERS
51 $m->suppress_errors
52 Set to true to turn off warnings when evaluating expressions
53
54 $m->last_error
55 If the last evaluation failed, contains a string describing the error.
56 (Useful when suppress_errors is on).
57
58AUTHOR INFORMATION
59 Copyright 2005, Miles Kaufmann.
60
61LICENSE
62 Redistribution and use in source and binary forms, with or without
63 modification, are permitted provided that the following conditions are
64 met:
65
66 1 Redistributions of source code must retain the above copyright
67 notice, this list of conditions and the following disclaimer.
68 2. Redistributions in binary form must reproduce the above copyright
69 notice, this list of conditions and the following disclaimer in the
70 documentation and/or other materials provided with the distribution.
71 3. The name of the author may not be used to endorse or promote
72 products derived from this software without specific prior written
73 permission.
74
75 THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
76 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
77 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
78 DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
79 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
80 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
81 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
82 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
83 STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
84 ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
85 POSSIBILITY OF SUCH DAMAGE.
86
87*/
88
90{
91 public bool $suppress_errors = false;
92 public ?string $last_error = null;
93
94 public array $v = array('e' => 2.71,'pi' => 3.14); // variables (and constants)
95 public array $f = []; // user-defined functions
96 public array $vb = array('e', 'pi'); // constants
97 public array $fb = array( // built-in functions
98 'sin','sinh','arcsin','asin','arcsinh','asinh',
99 'cos','cosh','arccos','acos','arccosh','acosh',
100 'tan','tanh','arctan','atan','arctanh','atanh',
101 'sqrt','abs','ln','log');
102
103 public function __construct()
104 {
105 // make the variables a little more accurate
106 $this->v['pi'] = pi();
107 $this->v['exp'] = exp(1);
108 $this->v['e'] = exp(1); // different result for exp(1) and e
109 $this->fb[] = 'exp'; // usage of php exp function in formula
110 }
111
112 public function e(string $expr)
113 {
114 return $this->evaluate($expr);
115 }
116
117 public function evaluate(string $expr)
118 {
119 // convert exponential notation
120 $expr = preg_replace_callback(
121 "/(\\d{0,1})e(-{0,1}\\d+)/is",
122 fn ($hit): string => $hit[1] . ((strlen($hit[1])) ? '*' : '') . '10^(' . $hit[2] . ')',
123 $expr
124 );
125 // standard functionality
126 $this->last_error = null;
127 $expr = trim($expr);
128 if (substr($expr, -1, 1) == ';') {
129 $expr = substr($expr, 0, strlen($expr) - 1);
130 } // strip semicolons at the end
131 //===============
132 // is it a variable assignment?
133 if (preg_match('/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches)) {
134 if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant
135 return $this->trigger("cannot assign to constant '$matches[1]'");
136 }
137 if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) {
138 return false;
139 } // get the result and make sure it's good
140 $this->v[$matches[1]] = $tmp; // if so, stick it in the variable array
141 return $this->v[$matches[1]]; // and return the resulting value
142 //===============
143 // is it a function assignment?
144 } elseif (preg_match('/^\s*([a-z]\w*)\s*\‍(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\‍)\s*=\s*(.+)$/', $expr, $matches)) {
145 $fnn = $matches[1]; // get the function name
146 if (in_array($matches[1], $this->fb)) { // make sure it isn't built in
147 return $this->trigger("cannot redefine built-in function '$matches[1]()'");
148 }
149 $args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments
150 if (($stack = $this->nfx($matches[3])) === false) {
151 return false;
152 } // see if it can be converted to postfix
153 for ($i = 0; $i < count($stack); $i++) { // freeze the state of the non-argument variables
154 $token = $stack[$i];
155 if (preg_match('/^[a-z]\w*$/', $token) and !in_array($token, $args)) {
156 if (array_key_exists($token, $this->v)) {
157 $stack[$i] = $this->v[$token];
158 } else {
159 return $this->trigger("undefined variable '$token' in function definition");
160 }
161 }
162 }
163 $this->f[$fnn] = array('args' => $args, 'func' => $stack);
164 return true;
165 } else {
166 return $this->pfx($this->nfx($expr)); // straight up evaluation, woo
167 }
168 }
169
170 public function vars(): array
171 {
172 $output = $this->v;
173 unset($output['pi']);
174 unset($output['e']);
175 return $output;
176 }
177
181 public function funcs(): array
182 {
183 $output = [];
184 foreach ($this->f as $fnn => $dat) {
185 $output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
186 }
187 return $output;
188 }
189
190 public function nfx($expr)
191 {
192 $index = 0;
193 $stack = new EvalMathStack();
194 $output = []; // postfix form of expression, to be passed to pfx()
195 $expr = trim(strtolower($expr));
196
197 $ops = array('+', '-', '*', '/', '^', '_');
198 $ops_r = array('+' => 0,'-' => 0,'*' => 0,'/' => 0,'^' => 1); // right-associative operator?
199 $ops_p = array('+' => 0,'-' => 0,'*' => 1,'/' => 1,'_' => 1,'^' => 2); // operator precedence
200
201 $expecting_op = false; // we use this in syntax-checking the expression
202 // and determining when a - is a negation
203
204 if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good
205 return $this->trigger("illegal character '{$matches[0]}'");
206 }
207
208 while (1) { // 1 Infinite Loop ;)
209 $op = substr($expr, $index, 1); // get the first character at the current index
210 // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
211 $ex = preg_match('/^([01]+[bB]|[\da-fA-F]+[hH]|[a-z]\w*\‍(?|\d+(?:\.\d*)?|\.\d+|\‍()/', substr($expr, $index), $match);
212 //===============
213 if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
214 $stack->push('_'); // put a negation on the stack
215 $index++;
216 } elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack
217 return $this->trigger("illegal character '_'"); // but not in the input expression
218 //===============
219 } elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack?
220 if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis?
221 $op = '*';
222 $index--; // it's an implicit multiplication
223 }
224 // heart of the algorithm:
225 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])) {
226 $output[] = $stack->pop(); // pop stuff off the stack into the output
227 }
228 // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
229 $stack->push($op); // finally put OUR operator onto the stack
230 $index++;
231 $expecting_op = false;
232 //===============
233 } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
234 while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last (
235 if (is_null($o2)) {
236 return $this->trigger("unexpected ')'");
237 } else {
238 $output[] = $o2;
239 }
240 }
241 if (preg_match("/^([a-z]\w*)\‍($/", (string) $stack->last(2), $matches)) { // did we just close a function?
242 $fnn = $matches[1]; // get the function name
243 $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
244 $output[] = $stack->pop(); // pop the function and push onto the output
245 if (in_array($fnn, $this->fb)) { // check the argument count
246 if ($arg_count > 1) {
247 return $this->trigger("too many arguments ($arg_count given, 1 expected)");
248 }
249 } elseif (array_key_exists($fnn, $this->f)) {
250 if ($arg_count != count($this->f[$fnn]['args'])) {
251 return $this->trigger("wrong number of arguments ($arg_count given, " . count($this->f[$fnn]['args']) . " expected)");
252 }
253 } else { // did we somehow push a non-function on the stack? this should never happen
254 return $this->trigger("internal error");
255 }
256 }
257 $index++;
258 //===============
259 } elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
260 while (($o2 = $stack->pop()) != '(') {
261 if (is_null($o2)) {
262 return $this->trigger("unexpected ','");
263 } // oops, never had a (
264 else {
265 $output[] = $o2;
266 } // pop the argument expression stuff and push onto the output
267 }
268 // make sure there was a function
269 if (!preg_match("/^([a-z]\w*)\‍($/", $stack->last(2), $matches)) {
270 return $this->trigger("unexpected ','");
271 }
272 $stack->push($stack->pop() + 1); // increment the argument count
273 $stack->push('('); // put the ( back on, we'll need to pop back to it again
274 $index++;
275 $expecting_op = false;
276 //===============
277 } elseif ($op == '(' and !$expecting_op) {
278 $stack->push('('); // that was easy
279 $index++;
280 $allow_neg = true;
281 //===============
282 } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
283 $expecting_op = true;
284 $val = $match[1];
285 if (preg_match("/^([a-z]\w*)\‍($/", $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
286 if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f)) { // it's a func
287 $stack->push($val);
288 $stack->push(1);
289 $stack->push('(');
290 $expecting_op = false;
291 } else { // it's a var w/ implicit multiplication
292 $val = $matches[1];
293 $output[] = $val;
294 }
295 } else { // it's a plain old var or num
296 $output[] = $val;
297 }
298 $index += strlen($val);
299 //===============
300 } elseif ($op == ')') { // miscellaneous error checking
301 return $this->trigger("unexpected ')'");
302 } elseif (in_array($op, $ops) and !$expecting_op) {
303 return $this->trigger("unexpected operator '$op'");
304 } else { // I don't even want to know what you did to get here
305 return $this->trigger("an unexpected error occured");
306 }
307 if ($index == strlen($expr)) {
308 if (in_array($op, $ops)) { // did we end with an operator? bad.
309 return $this->trigger("operator '$op' lacks operand");
310 } else {
311 break;
312 }
313 }
314 while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace
315 $index++; // into implicit multiplication if no operator is there)
316 }
317 }
318 while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output
319 if ($op == '(') {
320 return $this->trigger("expecting ')'");
321 } // if there are (s on the stack, ()s were unbalanced
322 $output[] = $op;
323 }
324 return $output;
325 }
326
327 // evaluate postfix notation
328 public function pfx($tokens, $vars = [])
329 {
330 if ($tokens == false) {
331 return false;
332 }
333
334 $stack = new EvalMathStack();
335
336 foreach ($tokens as $token) { // nice and easy
337 // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
338 if (in_array($token, array('+', '-', '*', '/', '^'))) {
339 if (is_null($op2 = $stack->pop())) {
340 return $this->trigger("internal error");
341 }
342 if (is_null($op1 = $stack->pop())) {
343 return $this->trigger("internal error");
344 }
345 switch ($token) {
346 case '+':
347 $stack->push(ilMath::_add($op1, $op2));
348 break;
349 case '-':
350 $stack->push(ilMath::_sub($op1, $op2));
351 break;
352 case '*':
353 $stack->push(ilMath::_mul($op1, $op2));
354 break;
355 case '/':
356 if ($op2 == 0) {
357 return $this->trigger("division by zero");
358 }
359 $stack->push(ilMath::_div($op1, $op2));
360 break;
361 case '^':
362 $stack->push(ilMath::_pow($op1, $op2));
363 break;
364 }
365 // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
366 } elseif ($token == "_") {
367 $stack->push(-1 * $stack->pop());
368 // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
369 } elseif (preg_match("/^([a-z]\w*)\‍($/", $token, $matches)) { // it's a function!
370 $fnn = $matches[1];
371 if (in_array($fnn, $this->fb)) { // built-in function:
372 if (is_null($op1 = $stack->pop())) {
373 return $this->trigger("internal error");
374 }
375 $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
376 if ($fnn == 'log') {
377 $fnn = 'log10';
378 } elseif ($fnn == 'ln') {
379 $fnn = 'log';
380 }
381
382 $stack->push($fnn($op1)); // 'eval()' can be easily avoided here
383 } elseif (array_key_exists($fnn, $this->f)) { // user function
384 // get args
385 $args = [];
386 for ($i = count($this->f[$fnn]['args']) - 1; $i >= 0; $i--) {
387 if (is_null($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) {
388 return $this->trigger("internal error");
389 }
390 }
391 $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
392 }
393 // if the token is a number or variable, push it on the stack
394 } else {
395 if (is_numeric($token)) {
396 $stack->push($token);
397 } elseif (($hex = $this->from_hexbin($token)) !== false) {
398 $stack->push($hex);
399 } elseif (array_key_exists($token, $this->v)) {
400 $stack->push($this->v[$token]);
401 } elseif (array_key_exists($token, $vars)) {
402 $stack->push($vars[$token]);
403 } else {
404 return $this->trigger("undefined variable '$token'");
405 }
406 }
407 }
408 // when we're out of tokens, the stack should have a single element, the final result
409 if ($stack->count != 1) {
410 return $this->trigger("internal error");
411 }
412 return $stack->pop();
413 }
414
415 // trigger an error, but nicely, if need be
416 public function trigger(string $msg): bool
417 {
418 $this->last_error = $msg;
419 if (!$this->suppress_errors) {
420 trigger_error($msg, E_USER_WARNING);
421 }
422 return false;
423 }
424
425 // check if the token is a hex/bin number, and convert to decimal
426 // 1234h/0101010b are allowed
427 public function from_hexbin($token)
428 {
429 if (strtoupper(substr($token, -1, 1)) == 'H') {
430 return hexdec($token);
431 }
432 if (strtoupper(substr($token, -1, 1)) == 'B') {
433 return bindec($token);
434 }
435 return false;
436 }
437}
438
439/******************************************************************************
440 *
441 * This file is part of ILIAS, a powerful learning management system.
442 *
443 * ILIAS is licensed with the GPL-3.0, you should have received a copy
444 * of said license along with the source code.
445 *
446 * If this is not the case or you just want to try ILIAS, you'll find
447 * us at:
448 * https://www.ilias.de
449 * https://github.com/ILIAS-eLearning
450 *
451 *****************************************************************************/
452// for internal use
454{
455 public array $stack = [];
456 public int $count = 0;
457
458 public function push($val): void
459 {
460 $this->stack[$this->count] = $val;
461 $this->count++;
462 }
463
464 public function pop()
465 {
466 if ($this->count > 0) {
467 $this->count--;
468 return $this->stack[$this->count];
469 }
470 return null;
471 }
472
473 public function last($n = 1)
474 {
475 if (isset($this->stack[$this->count - $n])) {
476 return $this->stack[$this->count - $n];
477 }
478 return null;
479 }
480}
bool $suppress_errors
string $last_error
pfx($tokens, $vars=[])
trigger(string $msg)
e(string $expr)
from_hexbin($token)
evaluate(string $expr)
static _sub($left_operand, $right_operand, int $scale=50)
static _pow($left_operand, $right_operand, int $scale=50)
static _add($left_operand, $right_operand, int $scale=50)
static _div($left_operand, $right_operand, int $scale=50)
static _mul($left_operand, $right_operand, int $scale=50)
$token
Definition: xapitoken.php:70