PluginProbe
TablePress – Tables in WordPress made easy / 3.2.8
TablePress – Tables in WordPress made easy v3.2.8
3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 2.4.4 All 44 releases
tablepress / libraries / evalmath.class.php

evalmath.class.php in TablePress – Tables in WordPress made easy 3.2.8, at libraries/evalmath.class.php

1,173 lines 36.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * EvalMath - Safely evaluate math expressions.
4 *
5 * Based on EvalMath by Miles Kaufmann, with modifications by Petr Skoda.
6 *
7 * @link https://github.com/moodle/moodle/blob/master/lib/evalmath/evalmath.class.php
8 *
9 * @package TablePress
10 * @subpackage Formulas
11 * @author Miles Kaufmann, Petr Skoda, Tobias Bäthge
12 * @since 1.0.0
13 */
14
15 // Prohibit direct script loading.
16 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
17
18 /**
19 * Class to safely evaluate math expressions.
20 *
21 * @package TablePress
22 * @subpackage Formulas
23 * @since 1.0.0
24 */
25 class EvalMath {
26
27 /**
28 * Pattern used for a valid function or variable name.
29 *
30 * Note, variable and function names are case insensitive.
31 *
32 * @since 1.0.0
33 */
34 protected static string $name_pattern = '[a-z][a-z0-9_]*';
35
36 /**
37 * Whether to suppress errors and warnings.
38 *
39 * @since 1.0.0
40 */
41 public bool $suppress_errors = false;
42
43 /**
44 * The last error message that was raised.
45 *
46 * @since 1.0.0
47 */
48 public string $last_error = '';
49
50 /**
51 * Variables (including constants).
52 *
53 * @since 1.0.0
54 * @var array<string, mixed>
55 */
56 public array $variables = array();
57
58 /**
59 * User-defined functions.
60 *
61 * @since 1.0.0
62 * @var array<string, mixed>
63 */
64 protected array $functions = array();
65
66 /**
67 * Constants.
68 *
69 * @since 1.0.0
70 * @var array<string, mixed>
71 */
72 protected array $constants = array();
73
74 /**
75 * Built-in functions.
76 *
77 * @since 1.0.0
78 * @var string[]
79 */
80 protected array $builtin_functions = array(
81 'sin',
82 'sinh',
83 'arcsin',
84 'asin',
85 'arcsinh',
86 'asinh',
87 'cos',
88 'cosh',
89 'arccos',
90 'acos',
91 'arccosh',
92 'acosh',
93 'tan',
94 'tanh',
95 'arctan',
96 'atan',
97 'arctanh',
98 'atanh',
99 'sqrt',
100 'abs',
101 'ln',
102 'log10',
103 'exp',
104 'floor',
105 'ceil',
106 );
107
108 /**
109 * Emulated functions.
110 *
111 * @since 1.0.0
112 * @var array<string, int[]>
113 */
114 protected array $calc_functions = array(
115 'average' => array( -1 ),
116 'mean' => array( -1 ),
117 'median' => array( -1 ),
118 'mode' => array( -1 ),
119 'range' => array( -1 ),
120 'max' => array( -1 ),
121 'min' => array( -1 ),
122 'mod' => array( 2 ),
123 'pi' => array( 0 ),
124 'power' => array( 2 ),
125 'log' => array( 1, 2 ),
126 'round' => array( 1, 2 ),
127 'number_format' => array( 1, 2 ),
128 'number_format_eu' => array( 1, 2 ),
129 'sum' => array( -1 ),
130 'counta' => array( -1 ),
131 'product' => array( -1 ),
132 'rand_int' => array( 2 ),
133 'rand_float' => array( 0 ),
134 'arctan2' => array( 2 ),
135 'atan2' => array( 2 ),
136 'if' => array( 3 ),
137 'not' => array( 1 ),
138 'and' => array( -1 ),
139 'or' => array( -1 ),
140 );
141
142 /**
143 * Class constructor.
144 *
145 * @since 1.0.0
146 */
147 public function __construct() {
148 // Set default constants.
149 $this->variables['pi'] = pi();
150 $this->variables['e'] = exp( 1 );
151 }
152
153 /**
154 * Evaluate a math expression without checking it for variable or function assignments.
155 *
156 * @since 1.0.0
157 *
158 * @param string $expression The expression that shall be evaluated.
159 * @return string|false Evaluated expression or false on error.
160 */
161 public function evaluate( $expression ) /* : string|false */ {
162 return $this->pfx( $this->nfx( $expression ) );
163 }
164
165 /**
166 * Evaluate a math expression or formula, and check it for variable and function assignments.
167 *
168 * @since 1.0.0
169 *
170 * @param string $expression The expression that shall be evaluated.
171 * @return string|bool Evaluated expression, true on successful function assignment, or false on error.
172 */
173 public function assign_and_evaluate( $expression ) /* : string|bool */ {
174 $this->last_error = '';
175 $expression = trim( $expression );
176 $expression = rtrim( $expression, ';' );
177
178 // Is the expression a variable assignment?
179 if ( 1 === preg_match( '/^\s*(' . self::$name_pattern . ')\s*=\s*(.+)$/', $expression, $matches ) ) {
180 // Make sure we're not assigning to a constant.
181 if ( in_array( $matches[1], $this->constants, true ) ) {
182 return $this->raise_error( 'cannot_assign_to_constant', $matches[1] );
183 }
184 // Evaluate the assignment.
185 $tmp = $this->pfx( $this->nfx( $matches[2] ) );
186 if ( false === $tmp ) {
187 return false;
188 }
189 // If it could be evaluated, add it to the variable array, ...
190 $this->variables[ $matches[1] ] = $tmp;
191 // ... and return the resulting value.
192 return $tmp;
193
194 // Is the expression a function assignment?
195 } elseif ( 1 === preg_match( '/^\s*(' . self::$name_pattern . ')\s*\(\s*(' . self::$name_pattern . '(?:\s*,\s*' . self::$name_pattern . ')*)\s*\)\s*=\s*(.+)$/', $expression, $matches ) ) {
196 // Get the function name.
197 $function_name = $matches[1];
198 // Make sure it isn't a built-in function -- we can't redefine those.
199 if ( in_array( $matches[1], $this->builtin_functions, true ) ) {
200 return $this->raise_error( 'cannot_redefine_builtin_function', $matches[1] );
201 }
202 // Get the function arguments after removing all whitespace.
203 $matches[2] = str_replace( array( "\n", "\r", "\t", ' ' ), '', $matches[2] );
204 $args = explode( ',', $matches[2] );
205
206 // Convert the function definition to postfix notation.
207 $stack = $this->nfx( $matches[3] );
208 if ( false === $stack ) {
209 return false;
210 }
211 // Freeze the state of the non-argument variables.
212 $stack_count = count( $stack );
213 for ( $i = 0; $i < $stack_count; $i++ ) {
214 $token = $stack[ $i ];
215 if ( 1 === preg_match( '/^' . self::$name_pattern . '$/', $token ) && ! in_array( $token, $args, true ) ) {
216 if ( array_key_exists( $token, $this->variables ) ) {
217 $stack[ $i ] = $this->variables[ $token ];
218 } else {
219 return $this->raise_error( 'undefined_variable_in_function_definition', $token );
220 }
221 }
222 }
223 $this->functions[ $function_name ] = array( 'args' => $args, 'func' => $stack );
224 return true;
225
226 // No variable or function assignment, so straight-up evaluation.
227 } else {
228 return $this->evaluate( $expression );
229 }
230 }
231
232 /**
233 * Return all user-defined variables and values.
234 *
235 * @since 1.0.0
236 *
237 * @return array<string, mixed> User-defined variables and values.
238 */
239 public function variables(): array {
240 return $this->variables;
241 }
242
243 /**
244 * Return all user-defined functions with their arguments.
245 *
246 * @since 1.0.0
247 *
248 * @return array<int, string> User-defined functions.
249 */
250 public function functions(): array {
251 $output = array();
252 foreach ( $this->functions as $name => $data ) {
253 $output[] = $name . '( ' . implode( ', ', $data['args'] ) . ' )';
254 }
255 return $output;
256 }
257
258 /*
259 * Internal methods.
260 */
261
262 /**
263 * Convert infix to postfix notation.
264 *
265 * @since 1.0.0
266 *
267 * @param string $expression Math expression that shall be converted.
268 * @return mixed[]|false Converted expression or false on error.
269 */
270 protected function nfx( $expression ) /* : mixed[]|false */ {
271 $index = 0;
272 $stack = new EvalMath_Stack();
273 $output = array(); // postfix form of expression, to be passed to pfx().
274 $expression = trim( strtolower( $expression ) );
275
276 $ops = array( '+', '-', '*', '/', '^', '_', '>', '<', '=', '%' );
277 $ops_r = array( '+' => 0, '-' => 0, '*' => 0, '/' => 0, '^' => 1, '>' => 0, '<' => 0, '=' => 0, '%' => 0 ); // Right-associative operator?
278 $ops_p = array( '+' => 0, '-' => 0, '*' => 1, '/' => 1, '_' => 1, '^' => 2, '>' => 0, '<' => 0, '=' => 0, '%' => 1 ); // Operator precedence.
279
280 // We use this in syntax-checking the expression and determining when a - (minus) is a negation.
281 $expecting_operator = false;
282
283 // Make sure the characters are all good.
284 if ( 1 === preg_match( '/[^\%\w\s+*^\/()\.,-<>=]/', $expression, $matches ) ) {
285 return $this->raise_error( 'illegal_character_general', $matches[0] );
286 }
287
288 // Infinite Loop for the conversion.
289 while ( true ) {
290 // Get the first character at the current index.
291 $op = substr( $expression, $index, 1 );
292 // Find out if we're currently at the beginning of a number/variable/function/parenthesis/operand.
293 $ex = preg_match( '/^(' . self::$name_pattern . '\(?|\d+(?:\.\d*)?(?:(e[+-]?)\d*)?|\.\d+|\()/', substr( $expression, $index ), $match );
294
295 // Is it a negation instead of a minus (in a subtraction)?
296 if ( '-' === $op && ! $expecting_operator ) {
297 // Put a negation on the stack.
298 $stack->push( '_' );
299 ++$index;
300 } elseif ( '_' === $op ) {
301 // We have to explicitly deny underscores (as they mean negation), because they are legal on the stack.
302 return $this->raise_error( 'illegal_character_underscore' );
303
304 // Are we putting an operator on the stack?
305 } elseif ( ( in_array( $op, $ops, true ) || $ex ) && $expecting_operator ) {
306 // Are we expecting an operator but have a number/variable/function/opening parenthesis?
307 if ( $ex ) {
308 // It's an implicit multiplication.
309 $op = '*';
310 --$index;
311 }
312 // Heart of the algorithm: .
313 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
314 while ( $stack->count > 0 && ( $o2 = $stack->last() ) && in_array( $o2, $ops, true ) && ( $ops_r[ $op ] ? $ops_p[ $op ] < $ops_p[ $o2 ] : $ops_p[ $op ] <= $ops_p[ $o2 ] ) ) {
315 // Pop stuff off the stack into the output.
316 $output[] = $stack->pop();
317 }
318 // Many thanks: https://en.wikipedia.org/wiki/Reverse_Polish_notation .
319 $stack->push( $op ); // Finally put OUR operator onto the stack.
320 ++$index;
321 $expecting_operator = false;
322
323 // Ready to close a parenthesis?
324 } elseif ( ')' === $op && $expecting_operator ) {
325 // Pop off the stack back to the last (.
326 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
327 while ( '(' !== ( $o2 = $stack->pop() ) ) {
328 if ( is_null( $o2 ) ) {
329 return $this->raise_error( 'unexpected_closing_bracket' );
330 } else {
331 $output[] = $o2;
332 }
333 }
334
335 // Did we just close a function?
336 if ( 1 === preg_match( '/^(' . self::$name_pattern . ')\($/', (string) $stack->last( 2 ), $matches ) ) {
337 // Get the function name.
338 $function_name = $matches[1];
339 // See how many arguments there were (cleverly stored on the stack, thank you).
340 $arg_count = $stack->pop();
341 $stack->pop(); // $fn
342 // Send function to output.
343 $output[] = array( 'function_name' => $function_name, 'arg_count' => $arg_count );
344 // Check the argument count, depending on what type of function we have.
345 if ( in_array( $function_name, $this->builtin_functions, true ) ) {
346 // Built-in functions.
347 if ( $arg_count > 1 ) { // @phpstan-ignore greater.alwaysFalse
348 $error_data = array( 'expected' => 1, 'given' => $arg_count );
349 return $this->raise_error( 'wrong_number_of_arguments', $error_data );
350 }
351 } elseif ( array_key_exists( $function_name, $this->calc_functions ) ) {
352 // Calc-emulation functions.
353 $counts = $this->calc_functions[ $function_name ];
354 // @phpstan-ignore greater.alwaysFalse, booleanAnd.alwaysFalse
355 if ( in_array( -1, $counts, true ) && $arg_count > 0 ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
356 // Everything is fine, we expected an indefinite number arguments and got some.
357 } elseif ( ! in_array( $arg_count, $counts, true ) ) { // @phpstan-ignore function.impossibleType
358 $error_data = array( 'expected' => implode( '/', $this->calc_functions[ $function_name ] ), 'given' => $arg_count );
359 return $this->raise_error( 'wrong_number_of_arguments', $error_data );
360 }
361 } elseif ( array_key_exists( $function_name, $this->functions ) ) {
362 // User-defined functions.
363 if ( count( $this->functions[ $function_name ]['args'] ) !== $arg_count ) { // @phpstan-ignore notIdentical.alwaysTrue
364 $error_data = array( 'expected' => count( $this->functions[ $function_name ]['args'] ), 'given' => $arg_count );
365 return $this->raise_error( 'wrong_number_of_arguments', $error_data );
366 }
367 } else {
368 // Did we somehow push a non-function on the stack? This should never happen.
369 return $this->raise_error( 'internal_error' );
370 }
371 }
372 ++$index;
373
374 // Did we just finish a function argument?
375 } elseif ( ',' === $op && $expecting_operator ) {
376 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
377 while ( '(' !== ( $o2 = $stack->pop() ) ) {
378 if ( is_null( $o2 ) ) {
379 // Oops, never had a (.
380 return $this->raise_error( 'unexpected_comma' );
381 } else {
382 // Pop the argument expression stuff and push onto the output.
383 $output[] = $o2;
384 }
385 }
386 // Make sure there was a function.
387 if ( 0 === preg_match( '/^(' . self::$name_pattern . ')\($/', (string) $stack->last( 2 ), $matches ) ) {
388 return $this->raise_error( 'unexpected_comma' );
389 }
390 // Increment the argument count.
391 $stack->push( $stack->pop() + 1 ); // @phpstan-ignore binaryOp.invalid
392 // Put the ( back on, we'll need to pop back to it again.
393 $stack->push( '(' );
394 ++$index;
395 $expecting_operator = false;
396
397 } elseif ( '(' === $op && ! $expecting_operator ) {
398 $stack->push( '(' ); // That was easy.
399 ++$index;
400
401 // Do we now have a function/variable/number?
402 } elseif ( $ex && ! $expecting_operator ) { // @phpstan-ignore booleanNot.alwaysTrue
403 $expecting_operator = true;
404 $value = $match[1];
405 // May be a function, or variable with implicit multiplication against parentheses...
406 if ( 1 === preg_match( '/^(' . self::$name_pattern . ')\($/', $value, $matches ) ) {
407 // Is it a function?
408 if ( in_array( $matches[1], $this->builtin_functions, true ) || array_key_exists( $matches[1], $this->functions ) || array_key_exists( $matches[1], $this->calc_functions ) ) {
409 $stack->push( $value );
410 $stack->push( 1 );
411 $stack->push( '(' );
412 $expecting_operator = false;
413 // It's a variable with implicit multiplication.
414 } else {
415 $value = $matches[1];
416 $output[] = $value;
417 }
418 } else {
419 // It's a plain old variable or number.
420 $output[] = $value;
421 }
422 $index += strlen( $value );
423
424 } elseif ( ')' === $op ) {
425 // It could be only custom function with no arguments or a general error.
426 if ( '(' !== $stack->last() || 1 !== $stack->last( 2 ) ) {
427 return $this->raise_error( 'unexpected_closing_bracket' );
428 }
429 // Did we just close a function?
430 if ( 1 === preg_match( '/^(' . self::$name_pattern . ')\($/', (string) $stack->last( 3 ), $matches ) ) {
431 $stack->pop(); // (
432 $stack->pop(); // 1
433 $stack->pop(); // $fn
434 // Get the function name.
435 $function_name = $matches[1];
436 if ( isset( $this->calc_functions[ $function_name ] ) ) {
437 // Custom calc-emulation function.
438 $counts = $this->calc_functions[ $function_name ];
439 } else {
440 // Default count for built-in functions.
441 $counts = array( 1 );
442 }
443 if ( ! in_array( 0, $counts, true ) ) {
444 $error_data = array( 'expected' => $counts, 'given' => 0 );
445 return $this->raise_error( 'wrong_number_of_arguments', $error_data );
446 }
447 // Send function to output.
448 $output[] = array( 'function_name' => $function_name, 'arg_count' => 0 );
449 ++$index;
450 $expecting_operator = true;
451 } else {
452 return $this->raise_error( 'unexpected_closing_bracket' );
453 }
454
455 // Miscellaneous error checking.
456 } elseif ( in_array( $op, $ops, true ) && ! $expecting_operator ) {
457 return $this->raise_error( 'unexpected_operator', $op );
458
459 // I don't even want to know what you did to get here.
460 } else {
461 return $this->raise_error( 'an_unexpected_error_occurred' );
462 }
463
464 if ( strlen( $expression ) === $index ) {
465 // Did we end with an operator? Bad.
466 if ( in_array( $op, $ops, true ) ) {
467 return $this->raise_error( 'operator_lacks_operand', $op );
468 } else {
469 break;
470 }
471 }
472
473 // Step the index past whitespace (pretty much turns whitespace into implicit multiplication if no operator is there).
474 while ( ' ' === substr( $expression, $index, 1 ) ) {
475 ++$index;
476 }
477 } // while ( true )
478
479 // Pop everything off the stack and push onto output.
480 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
481 while ( ! is_null( $op = $stack->pop() ) ) {
482 if ( '(' === $op ) {
483 // If there are (s on the stack, ()s were unbalanced.
484 return $this->raise_error( 'expecting_a_closing_bracket' );
485 }
486 $output[] = $op;
487 }
488
489 return $output;
490 }
491
492 /**
493 * Evaluate postfix notation.
494 *
495 * @since 1.0.0
496 *
497 * @param array<int, string|mixed[]>|false $tokens [description].
498 * @param array<string, mixed> $variables Optional. [description].
499 * @return mixed [description].
500 */
501 protected function pfx( $tokens, array $variables = array() ) /* : mixed */ {
502 if ( false === $tokens ) {
503 return false;
504 }
505
506 $stack = new EvalMath_Stack();
507
508 foreach ( $tokens as $token ) {
509 // If the token is a function, pop arguments off the stack, hand them to the function, and push the result back on.
510 if ( is_array( $token ) ) { // it's a function!
511 $function_name = $token['function_name'];
512 $count = $token['arg_count'];
513
514 if ( in_array( $function_name, $this->builtin_functions, true ) ) {
515 // Built-in function.
516
517 $op1 = $stack->pop();
518 if ( is_null( $op1 ) ) {
519 return $this->raise_error( 'internal_error' );
520 }
521 // For the "arc" trigonometric synonyms.
522 $function_name = preg_replace( '/^arc/', 'a', $function_name );
523 // Rewrite "ln" (only allows one argument) to "log" (natural logarithm).
524 if ( 'ln' === $function_name ) {
525 $function_name = 'log';
526 }
527 // Perfectly safe eval().
528 // phpcs:ignore Squiz.PHP.Eval.Discouraged
529 eval( '$stack->push( ' . $function_name . '( $op1 ) );' );
530 } elseif ( array_key_exists( $function_name, $this->calc_functions ) ) {
531 // Calc-emulation function.
532
533 // Get function arguments.
534 $args = array();
535 for ( $i = $count - 1; $i >= 0; $i-- ) {
536 $arg = $stack->pop();
537 if ( is_null( $arg ) ) {
538 return $this->raise_error( 'internal_error' );
539 } else {
540 $args[] = $arg;
541 }
542 }
543 // Rewrite some functions to their synonyms.
544 if ( 'if' === $function_name ) {
545 $function_name = 'func_if';
546 } elseif ( 'not' === $function_name ) {
547 $function_name = 'func_not';
548 } elseif ( 'and' === $function_name ) {
549 $function_name = 'func_and';
550 } elseif ( 'or' === $function_name ) {
551 $function_name = 'func_or';
552 } elseif ( 'mean' === $function_name ) {
553 $function_name = 'average';
554 } elseif ( 'arctan2' === $function_name ) {
555 $function_name = 'atan2';
556 }
557 $result = EvalMath_Functions::$function_name( ...array_reverse( $args ) );
558 if ( false === $result ) {
559 return $this->raise_error( 'internal_error' );
560 }
561 $stack->push( $result );
562 } elseif ( array_key_exists( $function_name, $this->functions ) ) {
563 // User-defined function.
564
565 // Get function arguments.
566 $args = array();
567 for ( $i = count( $this->functions[ $function_name ]['args'] ) - 1; $i >= 0; $i-- ) {
568 $arg = $stack->pop();
569 if ( is_null( $arg ) ) {
570 return $this->raise_error( 'internal_error' );
571 } else {
572 $args[ $this->functions[ $function_name ]['args'][ $i ] ] = $arg;
573 }
574 }
575 // Recursion.
576 $stack->push( $this->pfx( $this->functions[ $function_name ]['func'], $args ) );
577 }
578 } elseif ( in_array( $token, array( '+', '-', '*', '/', '^', '>', '<', '=', '%' ), true ) ) {
579 // If the token is a binary operator, pop two values off the stack, do the operation, and push the result back on.
580 $op2 = $stack->pop();
581 if ( is_null( $op2 ) ) {
582 return $this->raise_error( 'internal_error' );
583 }
584 $op1 = $stack->pop();
585 if ( is_null( $op1 ) ) {
586 return $this->raise_error( 'internal_error' );
587 }
588 switch ( $token ) {
589 case '+':
590 $stack->push( $op1 + $op2 );
591 break;
592 case '-':
593 $stack->push( $op1 - $op2 );
594 break;
595 case '*':
596 $stack->push( $op1 * $op2 );
597 break;
598 case '/':
599 if ( 0 === $op2 || '0' === $op2 ) {
600 return $this->raise_error( 'division_by_zero' );
601 }
602 $stack->push( $op1 / $op2 );
603 break;
604 case '^':
605 $stack->push( pow( $op1, $op2 ) );
606 break;
607 case '>':
608 $stack->push( (int) ( $op1 > $op2 ) );
609 break;
610 case '<':
611 $stack->push( (int) ( $op1 < $op2 ) );
612 break;
613 case '=':
614 // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison,Universal.Operators.StrictComparisons.LooseEqual
615 $stack->push( (int) ( $op1 == $op2 ) ); // Don't use === as the variable type can differ (int/double/bool).
616 break;
617 case '%':
618 $stack->push( $op1 % $op2 );
619 break;
620 }
621 } elseif ( '_' === $token ) {
622 // If the token is a unary operator, pop one value off the stack, do the operation, and push it back on.
623 $stack->push( -1 * $stack->pop() );
624 } elseif ( is_numeric( $token ) ) {
625 // If the token is a number, push it on the stack.
626 $stack->push( $token );
627 } elseif ( array_key_exists( $token, $this->variables ) ) {
628 // If the token is a variable, push it on the stack.
629 $stack->push( $this->variables[ $token ] );
630 } elseif ( array_key_exists( $token, $variables ) ) {
631 // If the token is a variable, push it on the stack.
632 $stack->push( $variables[ $token ] );
633 } else {
634 return $this->raise_error( 'undefined_variable', $token );
635 }
636 }
637 // When we're out of tokens, the stack should have a single element, the final result.
638 if ( 1 !== $stack->count ) {
639 return $this->raise_error( 'internal_error' );
640 }
641 return $stack->pop();
642 }
643
644 /**
645 * Raise an error.
646 *
647 * @since 1.0.0
648 *
649 * @param string $message Error message.
650 * @param mixed[]|string $error_data Optional. Additional error data.
651 * @return false False, to stop evaluation.
652 */
653 protected function raise_error( $message, $error_data = null ): bool {
654 $this->last_error = $this->get_error_string( $message, $error_data );
655 return false;
656 }
657
658 /**
659 * Get a translated string for an error message.
660 *
661 * @since 1.0.0
662 *
663 * @link https://github.com/moodle/moodle/blob/13264f35057d2f37374ec3e0e8ad4070f4676bd7/lang/en/mathslib.php
664 * @link https://github.com/moodle/moodle/blob/8e54ce9717c19f768b95f4332f70e3180ffafc46/lib/moodlelib.php#L6323
665 *
666 * @param string $identifier Identifier of the string.
667 * @param mixed[]|string $error_data Optional. Additional error data.
668 * @return string Translated string.
669 */
670 protected function get_error_string( $identifier, $error_data = null ): string {
671 $strings = array();
672 $strings['an_unexpected_error_occurred'] = 'an unexpected error occurred';
673 $strings['cannot_assign_to_constant'] = 'cannot assign to constant \'{$error_data}\'';
674 $strings['cannot_redefine_builtin_function'] = 'cannot redefine built-in function \'{$error_data}()\'';
675 $strings['division_by_zero'] = 'division by zero';
676 $strings['expecting_a_closing_bracket'] = 'expecting a closing bracket';
677 $strings['illegal_character_general'] = 'illegal character \'{$error_data}\'';
678 $strings['illegal_character_underscore'] = 'illegal character \'_\'';
679 $strings['internal_error'] = 'internal error';
680 $strings['operator_lacks_operand'] = 'operator \'{$error_data}\' lacks operand';
681 $strings['undefined_variable'] = 'undefined variable \'{$error_data}\'';
682 $strings['undefined_variable_in_function_definition'] = 'undefined variable \'{$error_data}\' in function definition';
683 $strings['unexpected_closing_bracket'] = 'unexpected closing bracket';
684 $strings['unexpected_comma'] = 'unexpected comma';
685 $strings['unexpected_operator'] = 'unexpected operator \'{$error_data}\'';
686 $strings['wrong_number_of_arguments'] = 'wrong number of arguments ({$error_data->given} given, {$error_data->expected} expected)';
687
688 $a_string = $strings[ $identifier ];
689
690 if ( null !== $error_data ) {
691 if ( is_array( $error_data ) ) {
692 $search = array();
693 $replace = array();
694 foreach ( $error_data as $key => $value ) {
695 if ( is_int( $key ) ) {
696 // We do not support numeric keys!
697 continue;
698 }
699 if ( is_object( $value ) || is_array( $value ) ) {
700 $value = (array) $value;
701 if ( count( $value ) > 1 ) {
702 $value = implode( ' or ', $value );
703 } else {
704 $value = (string) $value[0];
705 if ( '-1' === $value ) {
706 $value = 'at least 1';
707 }
708 }
709 }
710 $search[] = '{$error_data->' . $key . '}';
711 $replace[] = (string) $value;
712 }
713 if ( $search ) {
714 $a_string = str_replace( $search, $replace, $a_string );
715 }
716 } else {
717 $a_string = str_replace( '{$error_data}', (string) $error_data, $a_string );
718 }
719 }
720
721 return $a_string;
722 }
723
724 } // class EvalMath
725
726 /**
727 * Stack for the postfix/infix conversion of math expressions.
728 *
729 * @package TablePress
730 * @subpackage Formulas
731 * @since 1.0.0
732 */
733 class EvalMath_Stack { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound,Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
734
735 /**
736 * The stack.
737 *
738 * @since 1.0.0
739 * @var mixed[]
740 */
741 protected array $stack = array();
742
743 /**
744 * Number of items on the stack.
745 *
746 * @since 1.0.0
747 */
748 public int $count = 0;
749
750 /**
751 * Push an item onto the stack.
752 *
753 * @since 1.0.0
754 *
755 * @param mixed $value The item that is pushed onto the stack.
756 */
757 public function push( $value ): void {
758 $this->stack[ $this->count ] = $value;
759 ++$this->count;
760 }
761
762 /**
763 * Pop an item from the top of the stack.
764 *
765 * @since 1.0.0
766 *
767 * @return mixed|null The item that is popped from the stack.
768 */
769 public function pop() /* : mixed|null */ {
770 if ( $this->count > 0 ) {
771 --$this->count;
772 return $this->stack[ $this->count ];
773 }
774 return null;
775 }
776
777 /**
778 * Pop an item from the end of the stack.
779 *
780 * @since 1.0.0
781 *
782 * @param int $n Count from the end of the stack.
783 * @return mixed|null The item that is popped from the stack.
784 */
785 public function last( $n = 1 ) /* : mixed|null */ {
786 if ( ( $this->count - $n ) >= 0 ) {
787 return $this->stack[ $this->count - $n ];
788 }
789 return null;
790 }
791
792 } // class EvalMath_Stack
793
794 /**
795 * Common math functions, prepared for usage in EvalMath.
796 *
797 * @package TablePress
798 * @subpackage EvalMath
799 * @since 1.0.0
800 */
801 class EvalMath_Functions { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound,Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
802
803 /**
804 * Seed for the generation of random numbers.
805 *
806 * @since 1.0.0
807 */
808 protected static ?string $random_seed = null;
809
810 /**
811 * Choose from two values based on an if-condition.
812 *
813 * "if" is not a valid function name, which is why it's prefixed with "func_".
814 *
815 * @since 1.0.0
816 *
817 * @param double|int $condition Condition.
818 * @param double|int $statement Return value if the condition is true.
819 * @param double|int $alternative Return value if the condition is false.
820 * @return double|int Result of the if check.
821 */
822 public static function func_if( $condition, $statement, $alternative ) /* : float|int */ {
823 return ( (bool) $condition ? $statement : $alternative );
824 }
825
826 /**
827 * Return the negation (boolean "not") of a value.
828 *
829 * Similar to "func_if", the function name is prefixed with "func_", although it wouldn't be necessary.
830 *
831 * @since 1.0.0
832 *
833 * @param double|int $value Value to be negated.
834 * @return int Negated value (0 for false, 1 for true).
835 */
836 public static function func_not( $value ): int {
837 return (int) ! (bool) $value;
838 }
839
840 /**
841 * Calculate the conjunction (boolean "and") of some values.
842 *
843 * "and" is not a valid function name, which is why it's prefixed with "func_".
844 *
845 * @since 1.0.0
846 *
847 * @param double|int ...$args Values for which the conjunction shall be calculated.
848 * @return int Conjunction of the passed arguments.
849 */
850 public static function func_and( ...$args ): int {
851 foreach ( $args as $value ) {
852 if ( ! $value ) {
853 return 0;
854 }
855 }
856 return 1;
857 }
858
859 /**
860 * Calculate the disjunction (boolean "or") of some values.
861 *
862 * "or" is not a valid function name, which is why it's prefixed with "func_".
863 *
864 * @since 1.0.0
865 *
866 * @param double|int ...$args Values for which the disjunction shall be calculated.
867 * @return int Disjunction of the passed arguments.
868 */
869 public static function func_or( ...$args ): int {
870 foreach ( $args as $value ) {
871 if ( $value ) {
872 return 1;
873 }
874 }
875 return 0;
876 }
877
878 /**
879 * Return the (rounded) value of Pi.
880 *
881 * @since 1.0.0
882 *
883 * @return double Rounded value of Pi.
884 */
885 public static function pi(): float {
886 return pi();
887 }
888
889 /**
890 * Calculate the sum of the arguments.
891 *
892 * @since 1.0.0
893 *
894 * @param double|int ...$args Values for which the sum shall be calculated.
895 * @return double|int Sum of the passed arguments.
896 */
897 public static function sum( ...$args ) /* : float|int */ {
898 return array_sum( $args );
899 }
900
901 /**
902 * Count the number of non-empty arguments.
903 *
904 * @since 1.10.0
905 *
906 * @param double|int ...$args Values for which the number of non-empty elements shall be counted.
907 * @return int Counted number of non-empty elements in the passed values.
908 */
909 public static function counta( ...$args ): int {
910 return count( array_filter( $args ) );
911 }
912
913 /**
914 * Calculate the product of the arguments.
915 *
916 * @since 1.0.0
917 *
918 * @param double|int ...$args Values for which the product shall be calculated.
919 * @return double|int Product of the passed arguments.
920 */
921 public static function product( ...$args ) /* : float|int */ {
922 return array_product( $args );
923 }
924
925 /**
926 * Calculate the average/mean value of the arguments.
927 *
928 * @since 1.0.0
929 *
930 * @param double|int ...$args Values for which the average shall be calculated.
931 * @return double|int Average value of the passed arguments.
932 */
933 public static function average( ...$args ) /* : float|int */ {
934 // Catch division by zero.
935 if ( 0 === count( $args ) ) {
936 return 0;
937 }
938 return array_sum( $args ) / count( $args );
939 }
940
941 /**
942 * Calculate the median of the arguments.
943 *
944 * For even counts of arguments, the upper median is returned.
945 *
946 * @since 1.0.0
947 *
948 * @param array<int, double|int> ...$args Values for which the median shall be calculated.
949 * @return double|int Median of the passed arguments.
950 */
951 public static function median( array ...$args ) /* : float|int */ {
952 sort( $args );
953 $middle = intdiv( count( $args ), 2 ); // Upper median for even counts.
954 return $args[ $middle ]; // @phpstan-ignore return.type
955 }
956
957 /**
958 * Calculate the mode of the arguments.
959 *
960 * @since 1.0.0
961 *
962 * @param array<int, double|int> ...$args Values for which the mode shall be calculated.
963 * @return double|int Mode of the passed arguments.
964 */
965 public static function mode( ...$args ) /* : float|int */ {
966 $values = array_count_values( $args ); // @phpstan-ignore argument.type
967 asort( $values );
968 return array_key_last( $values ); // @phpstan-ignore return.type
969 }
970
971 /**
972 * Calculate the range of the arguments.
973 *
974 * @since 1.0.0
975 *
976 * @param double|int ...$args Values for which the range shall be calculated.
977 * @return double|int Range of the passed arguments.
978 */
979 public static function range( ...$args ) /* : float|int */ {
980 sort( $args );
981 return end( $args ) - reset( $args );
982 }
983
984 /**
985 * Find the maximum value of the arguments.
986 *
987 * @since 1.0.0
988 *
989 * @param double|int ...$args Values for which the maximum value shall be found.
990 * @return double|int Maximum value of the passed arguments.
991 */
992 public static function max( ...$args ) /* : float|int */ {
993 return max( $args ); // @phpstan-ignore argument.type
994 }
995
996 /**
997 * Find the minimum value of the arguments.
998 *
999 * @since 1.0.0
1000 *
1001 * @param double|int ...$args Values for which the minimum value shall be found.
1002 * @return double|int Minimum value of the passed arguments.
1003 */
1004 public static function min( ...$args ) /* : float|int */ {
1005 return min( $args ); // @phpstan-ignore argument.type
1006 }
1007
1008 /**
1009 * Calculate the remainder of a division of two numbers.
1010 *
1011 * @since 1.0.0
1012 *
1013 * @param double|int $op1 First number (dividend).
1014 * @param double|int $op2 Second number (divisor).
1015 * @return int Remainder of the division (dividend / divisor).
1016 */
1017 public static function mod( $op1, $op2 ): int {
1018 return $op1 % $op2;
1019 }
1020
1021 /**
1022 * Calculate the power of a base and an exponent.
1023 *
1024 * @since 1.0.0
1025 *
1026 * @param double|int $base Base.
1027 * @param double|int $exponent Exponent.
1028 * @return double|int Power base^exponent.
1029 */
1030 public static function power( $base, $exponent ) /* : float|int */ {
1031 return pow( $base, $exponent );
1032 }
1033
1034 /**
1035 * Calculate the logarithm of a number to a base.
1036 *
1037 * @since 1.0.0
1038 *
1039 * @param double|int $number Number.
1040 * @param double|int $base Optional. Base for the logarithm. Default e (for the natural logarithm).
1041 * @return double Logarithm of the number to the base.
1042 */
1043 public static function log( $number, $base = M_E ): float {
1044 return log( $number, $base );
1045 }
1046
1047 /**
1048 * Calculate the arc tangent of two variables.
1049 *
1050 * The signs of the numbers determine the quadrant of the result.
1051 *
1052 * @since 1.0.0
1053 *
1054 * @param double|int $op1 First number.
1055 * @param double|int $op2 Second number.
1056 * @return double Arc tangent of two numbers, similar to arc tangent of $op1/op$ except for the sign.
1057 */
1058 public static function atan2( $op1, $op2 ): float {
1059 return atan2( $op1, $op2 );
1060 }
1061
1062 /**
1063 * Round a number to a given precision.
1064 *
1065 * @since 1.0.0
1066 *
1067 * @param double|int $value Number to be rounded.
1068 * @param int $decimals Optional. Number of decimals after the comma after the rounding.
1069 * @return double Rounded number.
1070 */
1071 public static function round( $value, $decimals = 0 ): float {
1072 return round( $value, $decimals );
1073 }
1074
1075 /**
1076 * Format a number with the . as the decimal separator and the , as the thousand separator, rounded to a precision.
1077 *
1078 * The is the common number format in English-language regions.
1079 *
1080 * @since 1.0.0
1081 *
1082 * @param double|int $value Number to be rounded and formatted.
1083 * @param int $decimals Optional. Number of decimals after the decimal separator after the rounding.
1084 * @return string Formatted number.
1085 */
1086 public static function number_format( $value, $decimals = 0 ): string {
1087 return number_format( $value, $decimals, '.', ',' );
1088 }
1089
1090 /**
1091 * Format a number with the , as the decimal separator and the space as the thousand separator, rounded to a precision.
1092 *
1093 * The is the common number format in non-English-language regions, mainly in Europe.
1094 *
1095 * @since 1.0.0
1096 *
1097 * @param double|int $value Number to be rounded and formatted.
1098 * @param int $decimals Optional. Number of decimals after the decimal separator after the rounding.
1099 * @return string Formatted number.
1100 */
1101 public static function number_format_eu( $value, $decimals = 0 ): string {
1102 return number_format( $value, $decimals, ',', ' ' );
1103 }
1104
1105 /**
1106 * Set the seed for the generation of random numbers.
1107 *
1108 * @since 1.0.0
1109 *
1110 * @param string $random_seed The seed.
1111 */
1112 protected static function _set_random_seed( $random_seed ): void {
1113 self::$random_seed = $random_seed;
1114 }
1115
1116 /**
1117 * Get the seed for the generation of random numbers.
1118 *
1119 * @since 1.0.0
1120 *
1121 * @return string The seed.
1122 */
1123 protected static function _get_random_seed(): string {
1124 if ( is_null( self::$random_seed ) ) {
1125 return microtime();
1126 }
1127 return self::$random_seed;
1128 }
1129
1130 /**
1131 * Get a random integer from a range.
1132 *
1133 * @since 1.0.0
1134 *
1135 * @param int $min Minimum value for the range.
1136 * @param int $max Maximum value for the range.
1137 * @return int Random integer from the range [$min, $max].
1138 */
1139 public static function rand_int( $min, $max ): int {
1140 // Swap min and max value if min is bigger than max.
1141 if ( $min > $max ) {
1142 $tmp = $max;
1143 $max = $min;
1144 $min = $tmp;
1145 unset( $tmp );
1146 }
1147 $number_characters = (int) ceil( log( $max + 1 - $min, 16 ) );
1148 $md5string = md5( self::_get_random_seed() );
1149 $offset = 0;
1150 do {
1151 while ( ( $offset + $number_characters ) > strlen( $md5string ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
1152 $md5string .= md5( $md5string );
1153 }
1154 $random_number = (int) hexdec( substr( $md5string, $offset, $number_characters ) );
1155 $offset += $number_characters;
1156 } while ( ( $min + $random_number ) > $max );
1157 return $min + $random_number;
1158 }
1159
1160 /**
1161 * Get a random double value from a range [0, 1].
1162 *
1163 * @since 1.0.0
1164 *
1165 * @return double Random number from the range [0, 1].
1166 */
1167 public static function rand_float(): float {
1168 $random_values = unpack( 'v', md5( self::_get_random_seed(), true ) );
1169 return array_shift( $random_values ) / 65536; // @phpstan-ignore argument.type
1170 }
1171
1172 } // class EvalMath_Functions
1173