PluginProbe
TablePress – Tables in WordPress made easy / 2.3.2
TablePress – Tables in WordPress made easy v2.3.2
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 2.3.2, at libraries/evalmath.class.php

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