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

1,177 lines 35.9 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 ) {
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 if ( in_array( -1, $counts, true ) && $arg_count > 0 ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
358 // Everything is fine, we expected an indefinite number arguments and got some.
359 } elseif ( ! in_array( $arg_count, $counts, true ) ) {
360 $error_data = array( 'expected' => implode( '/', $this->calc_functions[ $function_name ] ), 'given' => $arg_count );
361 return $this->raise_error( 'wrong_number_of_arguments', $error_data );
362 }
363 } elseif ( array_key_exists( $function_name, $this->functions ) ) {
364 // User-defined functions.
365 if ( count( $this->functions[ $function_name ]['args'] ) !== $arg_count ) {
366 $error_data = array( 'expected' => count( $this->functions[ $function_name ]['args'] ), 'given' => $arg_count );
367 return $this->raise_error( 'wrong_number_of_arguments', $error_data );
368 }
369 } else {
370 // Did we somehow push a non-function on the stack? This should never happen.
371 return $this->raise_error( 'internal_error' );
372 }
373 }
374 ++$index;
375
376 // Did we just finish a function argument?
377 } elseif ( ',' === $op && $expecting_operator ) {
378 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
379 while ( '(' !== ( $o2 = $stack->pop() ) ) {
380 if ( is_null( $o2 ) ) {
381 // Oops, never had a (.
382 return $this->raise_error( 'unexpected_comma' );
383 } else {
384 // Pop the argument expression stuff and push onto the output.
385 $output[] = $o2;
386 }
387 }
388 // Make sure there was a function.
389 if ( 0 === preg_match( '/^(' . self::$name_pattern . ')\($/', (string) $stack->last( 2 ), $matches ) ) {
390 return $this->raise_error( 'unexpected_comma' );
391 }
392 // Increment the argument count.
393 $stack->push( $stack->pop() + 1 );
394 // Put the ( back on, we'll need to pop back to it again.
395 $stack->push( '(' );
396 ++$index;
397 $expecting_operator = false;
398
399 } elseif ( '(' === $op && ! $expecting_operator ) {
400 $stack->push( '(' ); // That was easy.
401 ++$index;
402
403 // Do we now have a function/variable/number?
404 } elseif ( $ex && ! $expecting_operator ) {
405 $expecting_operator = true;
406 $value = $match[1];
407 // May be a function, or variable with implicit multiplication against parentheses...
408 if ( 1 === preg_match( '/^(' . self::$name_pattern . ')\($/', $value, $matches ) ) {
409 // Is it a function?
410 if ( in_array( $matches[1], $this->builtin_functions, true ) || array_key_exists( $matches[1], $this->functions ) || array_key_exists( $matches[1], $this->calc_functions ) ) {
411 $stack->push( $value );
412 $stack->push( 1 );
413 $stack->push( '(' );
414 $expecting_operator = false;
415 // It's a variable with implicit multiplication.
416 } else {
417 $value = $matches[1];
418 $output[] = $value;
419 }
420 } else {
421 // It's a plain old variable or number.
422 $output[] = $value;
423 }
424 $index += strlen( $value );
425
426 } elseif ( ')' === $op ) {
427 // It could be only custom function with no arguments or a general error.
428 if ( '(' !== $stack->last() || 1 !== $stack->last( 2 ) ) {
429 return $this->raise_error( 'unexpected_closing_bracket' );
430 }
431 // Did we just close a function?
432 if ( 1 === preg_match( '/^(' . self::$name_pattern . ')\($/', (string) $stack->last( 3 ), $matches ) ) {
433 $stack->pop(); // (
434 $stack->pop(); // 1
435 $stack->pop(); // $fn
436 // Get the function name.
437 $function_name = $matches[1];
438 if ( isset( $this->calc_functions[ $function_name ] ) ) {
439 // Custom calc-emulation function.
440 $counts = $this->calc_functions[ $function_name ];
441 } else {
442 // Default count for built-in functions.
443 $counts = array( 1 );
444 }
445 if ( ! in_array( 0, $counts, true ) ) {
446 $error_data = array( 'expected' => $counts, 'given' => 0 );
447 return $this->raise_error( 'wrong_number_of_arguments', $error_data );
448 }
449 // Send function to output.
450 $output[] = array( 'function_name' => $function_name, 'arg_count' => 0 );
451 ++$index;
452 $expecting_operator = true;
453 } else {
454 return $this->raise_error( 'unexpected_closing_bracket' );
455 }
456
457 // Miscellaneous error checking.
458 } elseif ( in_array( $op, $ops, true ) && ! $expecting_operator ) {
459 return $this->raise_error( 'unexpected_operator', $op );
460
461 // I don't even want to know what you did to get here.
462 } else {
463 return $this->raise_error( 'an_unexpected_error_occurred' );
464 }
465
466 if ( strlen( $expression ) === $index ) {
467 // Did we end with an operator? Bad.
468 if ( in_array( $op, $ops, true ) ) {
469 return $this->raise_error( 'operator_lacks_operand', $op );
470 } else {
471 break;
472 }
473 }
474
475 // Step the index past whitespace (pretty much turns whitespace into implicit multiplication if no operator is there).
476 while ( ' ' === substr( $expression, $index, 1 ) ) {
477 ++$index;
478 }
479 } // while ( true )
480
481 // Pop everything off the stack and push onto output.
482 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
483 while ( ! is_null( $op = $stack->pop() ) ) { // @phpstan-ignore-line
484 if ( '(' === $op ) {
485 // If there are (s on the stack, ()s were unbalanced.
486 return $this->raise_error( 'expecting_a_closing_bracket' );
487 }
488 $output[] = $op;
489 }
490
491 return $output;
492 }
493
494 /**
495 * Evaluate postfix notation.
496 *
497 * @since 1.0.0
498 *
499 * @param array<int, string|mixed[]>|false $tokens [description].
500 * @param array<string, mixed> $variables Optional. [description].
501 * @return mixed [description].
502 */
503 protected function pfx( $tokens, array $variables = array() ) /* : mixed */ {
504 if ( false === $tokens ) {
505 return false;
506 }
507
508 $stack = new EvalMath_Stack();
509
510 foreach ( $tokens as $token ) {
511 // If the token is a function, pop arguments off the stack, hand them to the function, and push the result back on.
512 if ( is_array( $token ) ) { // it's a function!
513 $function_name = $token['function_name'];
514 $count = $token['arg_count'];
515
516 if ( in_array( $function_name, $this->builtin_functions, true ) ) {
517 // Built-in function.
518
519 $op1 = $stack->pop();
520 if ( is_null( $op1 ) ) {
521 return $this->raise_error( 'internal_error' );
522 }
523 // For the "arc" trigonometric synonyms.
524 $function_name = preg_replace( '/^arc/', 'a', $function_name );
525 // Rewrite "ln" (only allows one argument) to "log" (natural logarithm).
526 if ( 'ln' === $function_name ) {
527 $function_name = 'log';
528 }
529 // Perfectly safe eval().
530 // phpcs:ignore Squiz.PHP.Eval.Discouraged
531 eval( '$stack->push( ' . $function_name . '( $op1 ) );' );
532 } elseif ( array_key_exists( $function_name, $this->calc_functions ) ) {
533 // Calc-emulation function.
534
535 // Get function arguments.
536 $args = array();
537 for ( $i = $count - 1; $i >= 0; $i-- ) {
538 $arg = $stack->pop();
539 if ( is_null( $arg ) ) {
540 return $this->raise_error( 'internal_error' );
541 } else {
542 $args[] = $arg;
543 }
544 }
545 // Rewrite some functions to their synonyms.
546 if ( 'if' === $function_name ) {
547 $function_name = 'func_if';
548 } elseif ( 'not' === $function_name ) {
549 $function_name = 'func_not';
550 } elseif ( 'and' === $function_name ) {
551 $function_name = 'func_and';
552 } elseif ( 'or' === $function_name ) {
553 $function_name = 'func_or';
554 } elseif ( 'mean' === $function_name ) {
555 $function_name = 'average';
556 } elseif ( 'arctan2' === $function_name ) {
557 $function_name = 'atan2';
558 }
559 $result = EvalMath_Functions::$function_name( ...array_reverse( $args ) );
560 if ( false === $result ) {
561 return $this->raise_error( 'internal_error' );
562 }
563 $stack->push( $result );
564 } elseif ( array_key_exists( $function_name, $this->functions ) ) {
565 // User-defined function.
566
567 // Get function arguments.
568 $args = array();
569 for ( $i = count( $this->functions[ $function_name ]['args'] ) - 1; $i >= 0; $i-- ) {
570 $arg = $stack->pop();
571 if ( is_null( $arg ) ) {
572 return $this->raise_error( 'internal_error' );
573 } else {
574 $args[ $this->functions[ $function_name ]['args'][ $i ] ] = $arg;
575 }
576 }
577 // yay... recursion!
578 $stack->push( $this->pfx( $this->functions[ $function_name ]['func'], $args ) ); // @phpstan-ignore-line
579 }
580 } elseif ( in_array( $token, array( '+', '-', '*', '/', '^', '>', '<', '=', '%' ), true ) ) {
581 // If the token is a binary operator, pop two values off the stack, do the operation, and push the result back on.
582 $op2 = $stack->pop();
583 if ( is_null( $op2 ) ) {
584 return $this->raise_error( 'internal_error' );
585 }
586 $op1 = $stack->pop();
587 if ( is_null( $op1 ) ) {
588 return $this->raise_error( 'internal_error' );
589 }
590 switch ( $token ) {
591 case '+':
592 $stack->push( $op1 + $op2 );
593 break;
594 case '-':
595 $stack->push( $op1 - $op2 );
596 break;
597 case '*':
598 $stack->push( $op1 * $op2 );
599 break;
600 case '/':
601 if ( 0 === $op2 || '0' === $op2 ) {
602 return $this->raise_error( 'division_by_zero' );
603 }
604 $stack->push( $op1 / $op2 );
605 break;
606 case '^':
607 $stack->push( pow( $op1, $op2 ) );
608 break;
609 case '>':
610 $stack->push( (int) ( $op1 > $op2 ) );
611 break;
612 case '<':
613 $stack->push( (int) ( $op1 < $op2 ) );
614 break;
615 case '=':
616 // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison,Universal.Operators.StrictComparisons.LooseEqual
617 $stack->push( (int) ( $op1 == $op2 ) ); // Don't use === as the variable type can differ (int/double/bool).
618 break;
619 case '%':
620 $stack->push( $op1 % $op2 );
621 break;
622 }
623 } elseif ( '_' === $token ) {
624 // If the token is a unary operator, pop one value off the stack, do the operation, and push it back on.
625 $stack->push( -1 * $stack->pop() );
626 } elseif ( is_numeric( $token ) ) {
627 // If the token is a number, push it on the stack.
628 $stack->push( $token );
629 } elseif ( array_key_exists( $token, $this->variables ) ) {
630 // If the token is a variable, push it on the stack.
631 $stack->push( $this->variables[ $token ] );
632 } elseif ( array_key_exists( $token, $variables ) ) {
633 // If the token is a variable, push it on the stack.
634 $stack->push( $variables[ $token ] );
635 } else {
636 return $this->raise_error( 'undefined_variable', $token );
637 }
638 }
639 // When we're out of tokens, the stack should have a single element, the final result.
640 if ( 1 !== $stack->count ) {
641 return $this->raise_error( 'internal_error' );
642 }
643 return $stack->pop();
644 }
645
646 /**
647 * Raise an error.
648 *
649 * @since 1.0.0
650 *
651 * @param string $message Error message.
652 * @param mixed[]|string $error_data Optional. Additional error data.
653 * @return false False, to stop evaluation.
654 */
655 protected function raise_error( $message, $error_data = null ): bool {
656 $this->last_error = $this->get_error_string( $message, $error_data );
657 return false;
658 }
659
660 /**
661 * Get a translated string for an error message.
662 *
663 * @since 1.0.0
664 *
665 * @link https://github.com/moodle/moodle/blob/13264f35057d2f37374ec3e0e8ad4070f4676bd7/lang/en/mathslib.php
666 * @link https://github.com/moodle/moodle/blob/8e54ce9717c19f768b95f4332f70e3180ffafc46/lib/moodlelib.php#L6323
667 *
668 * @param string $identifier Identifier of the string.
669 * @param mixed[]|string $error_data Optional. Additional error data.
670 * @return string Translated string.
671 */
672 protected function get_error_string( $identifier, $error_data = null ): string {
673 $strings = array();
674 $strings['an_unexpected_error_occurred'] = 'an unexpected error occurred';
675 $strings['cannot_assign_to_constant'] = 'cannot assign to constant \'{$error_data}\'';
676 $strings['cannot_redefine_builtin_function'] = 'cannot redefine built-in function \'{$error_data}()\'';
677 $strings['division_by_zero'] = 'division by zero';
678 $strings['expecting_a_closing_bracket'] = 'expecting a closing bracket';
679 $strings['illegal_character_general'] = 'illegal character \'{$error_data}\'';
680 $strings['illegal_character_underscore'] = 'illegal character \'_\'';
681 $strings['internal_error'] = 'internal error';
682 $strings['operator_lacks_operand'] = 'operator \'{$error_data}\' lacks operand';
683 $strings['undefined_variable'] = 'undefined variable \'{$error_data}\'';
684 $strings['undefined_variable_in_function_definition'] = 'undefined variable \'{$error_data}\' in function definition';
685 $strings['unexpected_closing_bracket'] = 'unexpected closing bracket';
686 $strings['unexpected_comma'] = 'unexpected comma';
687 $strings['unexpected_operator'] = 'unexpected operator \'{$error_data}\'';
688 $strings['wrong_number_of_arguments'] = 'wrong number of arguments ({$error_data->given} given, {$error_data->expected} expected)';
689
690 $a_string = $strings[ $identifier ];
691
692 if ( null !== $error_data ) {
693 if ( is_array( $error_data ) ) {
694 $search = array();
695 $replace = array();
696 foreach ( $error_data as $key => $value ) {
697 if ( is_int( $key ) ) {
698 // We do not support numeric keys!
699 continue;
700 }
701 if ( is_object( $value ) || is_array( $value ) ) {
702 $value = (array) $value;
703 if ( count( $value ) > 1 ) {
704 $value = implode( ' or ', $value );
705 } else {
706 $value = (string) $value[0];
707 if ( '-1' === $value ) {
708 $value = 'at least 1';
709 }
710 }
711 }
712 $search[] = '{$error_data->' . $key . '}';
713 $replace[] = (string) $value;
714 }
715 if ( $search ) {
716 $a_string = str_replace( $search, $replace, $a_string );
717 }
718 } else {
719 $a_string = str_replace( '{$error_data}', (string) $error_data, $a_string );
720 }
721 }
722
723 return $a_string;
724 }
725
726 } // class EvalMath
727
728 /**
729 * Stack for the postfix/infix conversion of math expressions.
730 *
731 * @package TablePress
732 * @subpackage Formulas
733 * @since 1.0.0
734 */
735 class EvalMath_Stack { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound,Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
736
737 /**
738 * The stack.
739 *
740 * @since 1.0.0
741 * @var mixed[]
742 */
743 protected $stack = array();
744
745 /**
746 * Number of items on the stack.
747 *
748 * @since 1.0.0
749 * @var int
750 */
751 public $count = 0;
752
753 /**
754 * Push an item onto the stack.
755 *
756 * @since 1.0.0
757 *
758 * @param mixed $value The item that is pushed onto the stack.
759 */
760 public function push( $value ): void {
761 $this->stack[ $this->count ] = $value;
762 ++$this->count;
763 }
764
765 /**
766 * Pop an item from the top of the stack.
767 *
768 * @since 1.0.0
769 *
770 * @return mixed|null The item that is popped from the stack.
771 */
772 public function pop() /* : mixed|null */ {
773 if ( $this->count > 0 ) {
774 --$this->count;
775 return $this->stack[ $this->count ];
776 }
777 return null;
778 }
779
780 /**
781 * Pop an item from the end of the stack.
782 *
783 * @since 1.0.0
784 *
785 * @param int $n Count from the end of the stack.
786 * @return mixed|null The item that is popped from the stack.
787 */
788 public function last( $n = 1 ) /* : mixed|null */ {
789 if ( ( $this->count - $n ) >= 0 ) {
790 return $this->stack[ $this->count - $n ];
791 }
792 return null;
793 }
794
795 } // class EvalMath_Stack
796
797 /**
798 * Common math functions, prepared for usage in EvalMath.
799 *
800 * @package TablePress
801 * @subpackage EvalMath
802 * @since 1.0.0
803 */
804 class EvalMath_Functions { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound,Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
805
806 /**
807 * Seed for the generation of random numbers.
808 *
809 * @since 1.0.0
810 * @var string|null
811 */
812 protected static $random_seed = null;
813
814 /**
815 * Choose from two values based on an if-condition.
816 *
817 * "if" is not a valid function name, which is why it's prefixed with "func_".
818 *
819 * @since 1.0.0
820 *
821 * @param double|int $condition Condition.
822 * @param double|int $statement Return value if the condition is true.
823 * @param double|int $alternative Return value if the condition is false.
824 * @return double|int Result of the if check.
825 */
826 public static function func_if( $condition, $statement, $alternative ) /* : float|int */ {
827 return ( (bool) $condition ? $statement : $alternative );
828 }
829
830 /**
831 * Return the negation (boolean "not") of a value.
832 *
833 * Similar to "func_if", the function name is prefixed with "func_", although it wouldn't be necessary.
834 *
835 * @since 1.0.0
836 *
837 * @param double|int $value Value to be negated.
838 * @return int Negated value (0 for false, 1 for true).
839 */
840 public static function func_not( $value ): int {
841 return (int) ! (bool) $value;
842 }
843
844 /**
845 * Calculate the conjunction (boolean "and") of some values.
846 *
847 * "and" is not a valid function name, which is why it's prefixed with "func_".
848 *
849 * @since 1.0.0
850 *
851 * @param double|int ...$args Values for which the conjunction shall be calculated.
852 * @return int Conjunction of the passed arguments.
853 */
854 public static function func_and( ...$args ): int {
855 foreach ( $args as $value ) {
856 if ( ! $value ) {
857 return 0;
858 }
859 }
860 return 1;
861 }
862
863 /**
864 * Calculate the disjunction (boolean "or") of some values.
865 *
866 * "or" is not a valid function name, which is why it's prefixed with "func_".
867 *
868 * @since 1.0.0
869 *
870 * @param double|int ...$args Values for which the disjunction shall be calculated.
871 * @return int Disjunction of the passed arguments.
872 */
873 public static function func_or( ...$args ): int {
874 foreach ( $args as $value ) {
875 if ( $value ) {
876 return 1;
877 }
878 }
879 return 0;
880 }
881
882 /**
883 * Return the (rounded) value of Pi.
884 *
885 * @since 1.0.0
886 *
887 * @return double Rounded value of Pi.
888 */
889 public static function pi(): float {
890 return pi();
891 }
892
893 /**
894 * Calculate the sum of the arguments.
895 *
896 * @since 1.0.0
897 *
898 * @param double|int ...$args Values for which the sum shall be calculated.
899 * @return double|int Sum of the passed arguments.
900 */
901 public static function sum( ...$args ) /* : float|int */ {
902 return array_sum( $args );
903 }
904
905 /**
906 * Count the number of non-empty arguments.
907 *
908 * @since 1.10.0
909 *
910 * @param double|int ...$args Values for which the number of non-empty elements shall be counted.
911 * @return int Counted number of non-empty elements in the passed values.
912 */
913 public static function counta( ...$args ): int {
914 return count( array_filter( $args ) );
915 }
916
917 /**
918 * Calculate the product of the arguments.
919 *
920 * @since 1.0.0
921 *
922 * @param double|int ...$args Values for which the product shall be calculated.
923 * @return double|int Product of the passed arguments.
924 */
925 public static function product( ...$args ) /* : float|int */ {
926 return array_product( $args );
927 }
928
929 /**
930 * Calculate the average/mean value of the arguments.
931 *
932 * @since 1.0.0
933 *
934 * @param double|int ...$args Values for which the average shall be calculated.
935 * @return double|int Average value of the passed arguments.
936 */
937 public static function average( ...$args ) /* : float|int */ {
938 // Catch division by zero.
939 if ( 0 === count( $args ) ) {
940 return 0;
941 }
942 return array_sum( $args ) / count( $args );
943 }
944
945 /**
946 * Calculate the median of the arguments.
947 *
948 * For even counts of arguments, the upper median is returned.
949 *
950 * @since 1.0.0
951 *
952 * @param array<int, double|int> ...$args Values for which the median shall be calculated.
953 * @return double|int Median of the passed arguments.
954 */
955 public static function median( array ...$args ) /* : float|int */ {
956 sort( $args );
957 $middle = intdiv( count( $args ), 2 ); // Upper median for even counts.
958 return $args[ $middle ]; // @phpstan-ignore-line
959 }
960
961 /**
962 * Calculate the mode of the arguments.
963 *
964 * @since 1.0.0
965 *
966 * @param array<int, double|int> ...$args Values for which the mode shall be calculated.
967 * @return double|int Mode of the passed arguments.
968 */
969 public static function mode( ...$args ) /* : float|int */ {
970 $values = array_count_values( $args );
971 asort( $values );
972 return array_key_last( $values ); // @phpstan-ignore-line
973 }
974
975 /**
976 * Calculate the range of the arguments.
977 *
978 * @since 1.0.0
979 *
980 * @param double|int ...$args Values for which the range shall be calculated.
981 * @return double|int Range of the passed arguments.
982 */
983 public static function range( ...$args ) /* : float|int */ {
984 sort( $args );
985 return end( $args ) - reset( $args );
986 }
987
988 /**
989 * Find the maximum value of the arguments.
990 *
991 * @since 1.0.0
992 *
993 * @param double|int ...$args Values for which the maximum value shall be found.
994 * @return double|int Maximum value of the passed arguments.
995 */
996 public static function max( ...$args ) /* : float|int */ {
997 return max( $args );
998 }
999
1000 /**
1001 * Find the minimum value of the arguments.
1002 *
1003 * @since 1.0.0
1004 *
1005 * @param double|int ...$args Values for which the minimum value shall be found.
1006 * @return double|int Minimum value of the passed arguments.
1007 */
1008 public static function min( ...$args ) /* : float|int */ {
1009 return min( $args );
1010 }
1011
1012 /**
1013 * Calculate the remainder of a division of two numbers.
1014 *
1015 * @since 1.0.0
1016 *
1017 * @param double|int $op1 First number (dividend).
1018 * @param double|int $op2 Second number (divisor).
1019 * @return int Remainder of the division (dividend / divisor).
1020 */
1021 public static function mod( $op1, $op2 ): int {
1022 return $op1 % $op2;
1023 }
1024
1025 /**
1026 * Calculate the power of a base and an exponent.
1027 *
1028 * @since 1.0.0
1029 *
1030 * @param double|int $base Base.
1031 * @param double|int $exponent Exponent.
1032 * @return double|int Power base^exponent.
1033 */
1034 public static function power( $base, $exponent ) /* : float|int */ {
1035 return pow( $base, $exponent );
1036 }
1037
1038 /**
1039 * Calculate the logarithm of a number to a base.
1040 *
1041 * @since 1.0.0
1042 *
1043 * @param double|int $number Number.
1044 * @param double|int $base Optional. Base for the logarithm. Default e (for the natural logarithm).
1045 * @return double Logarithm of the number to the base.
1046 */
1047 public static function log( $number, $base = M_E ): float {
1048 return log( $number, $base );
1049 }
1050
1051 /**
1052 * Calculate the arc tangent of two variables.
1053 *
1054 * The signs of the numbers determine the quadrant of the result.
1055 *
1056 * @since 1.0.0
1057 *
1058 * @param double|int $op1 First number.
1059 * @param double|int $op2 Second number.
1060 * @return double Arc tangent of two numbers, similar to arc tangent of $op1/op$ except for the sign.
1061 */
1062 public static function atan2( $op1, $op2 ): float {
1063 return atan2( $op1, $op2 );
1064 }
1065
1066 /**
1067 * Round a number to a given precision.
1068 *
1069 * @since 1.0.0
1070 *
1071 * @param double|int $value Number to be rounded.
1072 * @param int $decimals Optional. Number of decimals after the comma after the rounding.
1073 * @return double Rounded number.
1074 */
1075 public static function round( $value, $decimals = 0 ): float {
1076 return round( $value, $decimals );
1077 }
1078
1079 /**
1080 * Format a number with the . as the decimal separator and the , as the thousand separator, rounded to a precision.
1081 *
1082 * The is the common number format in English-language regions.
1083 *
1084 * @since 1.0.0
1085 *
1086 * @param double|int $value Number to be rounded and formatted.
1087 * @param int $decimals Optional. Number of decimals after the decimal separator after the rounding.
1088 * @return string Formatted number.
1089 */
1090 public static function number_format( $value, $decimals = 0 ): string {
1091 return number_format( $value, $decimals, '.', ',' );
1092 }
1093
1094 /**
1095 * Format a number with the , as the decimal separator and the space as the thousand separator, rounded to a precision.
1096 *
1097 * The is the common number format in non-English-language regions, mainly in Europe.
1098 *
1099 * @since 1.0.0
1100 *
1101 * @param double|int $value Number to be rounded and formatted.
1102 * @param int $decimals Optional. Number of decimals after the decimal separator after the rounding.
1103 * @return string Formatted number.
1104 */
1105 public static function number_format_eu( $value, $decimals = 0 ): string {
1106 return number_format( $value, $decimals, ',', ' ' );
1107 }
1108
1109 /**
1110 * Set the seed for the generation of random numbers.
1111 *
1112 * @since 1.0.0
1113 *
1114 * @param string $random_seed The seed.
1115 */
1116 protected static function _set_random_seed( $random_seed ): void {
1117 self::$random_seed = $random_seed;
1118 }
1119
1120 /**
1121 * Get the seed for the generation of random numbers.
1122 *
1123 * @since 1.0.0
1124 *
1125 * @return string The seed.
1126 */
1127 protected static function _get_random_seed(): string {
1128 if ( is_null( self::$random_seed ) ) {
1129 return microtime();
1130 }
1131 return self::$random_seed;
1132 }
1133
1134 /**
1135 * Get a random integer from a range.
1136 *
1137 * @since 1.0.0
1138 *
1139 * @param int $min Minimum value for the range.
1140 * @param int $max Maximum value for the range.
1141 * @return int Random integer from the range [$min, $max].
1142 */
1143 public static function rand_int( $min, $max ): int {
1144 // Swap min and max value if min is bigger than max.
1145 if ( $min > $max ) {
1146 $tmp = $max;
1147 $max = $min;
1148 $min = $tmp;
1149 unset( $tmp );
1150 }
1151 $number_characters = (int) ceil( log( $max + 1 - $min, 16 ) );
1152 $md5string = md5( self::_get_random_seed() );
1153 $offset = 0;
1154 do {
1155 while ( ( $offset + $number_characters ) > strlen( $md5string ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
1156 $md5string .= md5( $md5string );
1157 }
1158 $random_number = (int) hexdec( substr( $md5string, $offset, $number_characters ) );
1159 $offset += $number_characters;
1160 } while ( ( $min + $random_number ) > $max );
1161 return $min + $random_number;
1162 }
1163
1164 /**
1165 * Get a random double value from a range [0, 1].
1166 *
1167 * @since 1.0.0
1168 *
1169 * @return double Random number from the range [0, 1].
1170 */
1171 public static function rand_float(): float {
1172 $random_values = unpack( 'v', md5( self::_get_random_seed(), true ) );
1173 return array_shift( $random_values ) / 65536; // @phpstan-ignore-line
1174 }
1175
1176 } // class EvalMath_Functions
1177