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