Projet

Général

Profil

Paste
Télécharger (26,5 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / ctools / includes / math-expr.inc @ 219d19c4

1 85ad3d82 Assos Assos
<?php
2
3 219d19c4 Assos Assos
/**
4
 * @file
5
 * =============================================================================.
6
 *
7
 * ctools_math_expr - PHP Class to safely evaluate math expressions
8
 * Copyright (C) 2005 Miles Kaufmann <http://www.twmagic.com/>
9
 *
10
 * =============================================================================
11
 *
12
 * NAME
13
 *     ctools_math_expr - safely evaluate math expressions
14
 *
15
 * SYNOPSIS
16
 *     $m = new ctools_math_expr();
17
 *     // basic evaluation:
18
 *     $result = $m->evaluate('2+2');
19
 *     // supports: order of operation; parentheses; negation; built-in
20
 *     // functions.
21
 *     $result = $m->evaluate('-8(5/2)^2*(1-sqrt(4))-8');
22
 *     // create your own variables
23
 *     $m->evaluate('a = e^(ln(pi))');
24
 *     // or functions
25
 *     $m->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1');
26
 *     // and then use them
27
 *     $result = $m->evaluate('3*f(42,a)');
28
 *
29
 * DESCRIPTION
30
 *     Use the ctools_math_expr class when you want to evaluate mathematical
31
 *     expressions from untrusted sources.  You can define your own variables
32
 *     and functions, which are stored in the object.  Try it, it's fun!
33
 *
34
 * AUTHOR INFORMATION
35
 *     Copyright 2005, Miles Kaufmann.
36
 *     Enhancements, 2005 onwards, Drupal Community.
37
 *
38
 * LICENSE
39
 *     Redistribution and use in source and binary forms, with or without
40
 *     modification, are permitted provided that the following conditions are
41
 *     met:
42
 *
43
 *     1   Redistributions of source code must retain the above copyright
44
 *         notice, this list of conditions and the following disclaimer.
45
 *     2.  Redistributions in binary form must reproduce the above copyright
46
 *         notice, this list of conditions and the following disclaimer in the
47
 *         documentation and/or other materials provided with the distribution.
48
 *     3.  The name of the author may not be used to endorse or promote
49
 *         products derived from this software without specific prior written
50
 *         permission.
51
 *
52
 *     THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
53
 *     IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
54
 *     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
55
 *     DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
56
 *     INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
57
 *     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
58
 *     SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
59
 *     HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
60
 *     STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
61
 *     ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
62
 *     POSSIBILITY OF SUCH DAMAGE.
63
 */
64
65
/**
66
 * ctools_math_expr Class.
67
 */
68 85ad3d82 Assos Assos
class ctools_math_expr {
69
70 c304a780 Assos Assos
  /**
71 219d19c4 Assos Assos
   * If TRUE do not call trigger_error on error other wise do.
72
   *
73
   * @var bool
74 c304a780 Assos Assos
   */
75 219d19c4 Assos Assos
  public $suppress_errors = FALSE;
76
77
  /**
78
   * The last error message reported.
79
   *
80
   * @var string
81
   */
82
  public $last_error = NULL;
83
84
  /**
85
   * List of all errors reported.
86
   *
87
   * @var array
88
   */
89
  public $errors = array();
90
91
  /**
92
   * Variable and constant values.
93
   *
94
   * @var array
95
   */
96
  protected $vars;
97
98
  /**
99
   * User defined functions.
100
   *
101
   * @var array
102
   */
103
  protected $userfuncs;
104
105
  /**
106
   * The names of constants, used to make constants read-only.
107
   *
108
   * @var array
109
   */
110
  protected $constvars;
111
112
  /**
113
   * Built in simple (one arg) functions.
114
   * Merged into $this->funcs in constructor.
115
   *
116
   * @var array
117
   */
118
  protected $simplefuncs;
119
120
  /**
121
   * Definitions of all built-in functions.
122
   *
123
   * @var array
124
   */
125
  protected $funcs;
126
127
  /**
128
   * Operators and their precedence.
129
   *
130
   * @var array
131
   */
132
  protected $ops;
133
134
  /**
135
   * The set of operators using two arguments.
136
   *
137
   * @var array
138
   */
139
  protected $binaryops;
140 85ad3d82 Assos Assos
141 219d19c4 Assos Assos
  /**
142
   * Public constructor.
143
   */
144
  public function __construct() {
145
    $this->userfuncs = array();
146
    $this->simplefuncs = array(
147
      'sin',
148
      'sinh',
149
      'asin',
150
      'asinh',
151
      'cos',
152
      'cosh',
153
      'acos',
154
      'acosh',
155
      'tan',
156
      'tanh',
157
      'atan',
158
      'atanh',
159
      'exp',
160
      'sqrt',
161
      'abs',
162
      'log',
163
      'ceil',
164
      'floor',
165
      'round',
166
    );
167
168
    $this->ops = array(
169
      '+' => array('precedence' => 0),
170
      '-' => array('precedence' => 0),
171
      '*' => array('precedence' => 1),
172
      '/' => array('precedence' => 1),
173
      '^' => array('precedence' => 2, 'right' => TRUE),
174
      '_' => array('precedence' => 1),
175
      '==' => array('precedence' => -1),
176
      '!=' => array('precedence' => -1),
177
      '>=' => array('precedence' => -1),
178
      '<=' => array('precedence' => -1),
179
      '>' => array('precedence' => -1),
180
      '<' => array('precedence' => -1),
181
    );
182
183
    $this->binaryops = array(
184
      '+', '-', '*', '/', '^', '==', '!=', '<', '<=', '>=', '>',
185
    );
186
187
    $this->funcs = array(
188
      'ln' => array(
189
        'function' => 'log',
190
        'arguments' => 1,
191
      ),
192
      'arcsin' => array(
193
        'function' => 'asin',
194
        'arguments' => 1,
195
      ),
196
      'arcsinh' => array(
197
        'function' => 'asinh',
198
        'arguments' => 1,
199
      ),
200
      'arccos' => array(
201
        'function' => 'acos',
202
        'arguments' => 1,
203
      ),
204
      'arccosh' => array(
205
        'function' => 'acosh',
206
        'arguments' => 1,
207
      ),
208
      'arctan' => array(
209
        'function' => 'atan',
210
        'arguments' => 1,
211
      ),
212
      'arctanh' => array(
213
        'function' => 'atanh',
214
        'arguments' => 1,
215
      ),
216
      'min' => array(
217
        'function' => 'min',
218
        'arguments' => 2,
219
        'max arguments' => 99,
220
      ),
221
      'max' => array(
222
        'function' => 'max',
223
        'arguments' => 2,
224
        'max arguments' => 99,
225
      ),
226
      'pow' => array(
227
        'function' => 'pow',
228
        'arguments' => 2,
229
      ),
230
      'if' => array(
231
        'function' => 'ctools_math_expr_if',
232
        'arguments' => 2,
233
        'max arguments' => 3,
234
      ),
235
      'number' => array(
236
        'function' => 'ctools_math_expr_number',
237
        'arguments' => 1,
238
      ),
239
      'time' => array(
240
        'function' => 'time',
241
        'arguments' => 0,
242
      ),
243
    );
244
245
    // Allow modules to add custom functions.
246
    $context = array('final' => &$this->funcs);
247
    drupal_alter('ctools_math_expression_functions', $this->simplefuncs, $context);
248
249
    // Set up the initial constants and mark them read-only.
250
    $this->vars = array('e' => exp(1), 'pi' => pi());
251
    drupal_alter('ctools_math_expression_constants', $this->vars);
252
    $this->constvars = array_keys($this->vars);
253
254
    // Translate the older, simpler style into the newer, richer style.
255
    foreach ($this->simplefuncs as $function) {
256
      $this->funcs[$function] = array(
257
        'function' => $function,
258
        'arguments' => 1,
259
      );
260 85ad3d82 Assos Assos
    }
261 219d19c4 Assos Assos
  }
262 85ad3d82 Assos Assos
263 219d19c4 Assos Assos
  /**
264
   * Change the suppress errors flag.
265
   *
266
   * When errors are not suppressed, trigger_error is used to cause a PHP error
267
   * when an evaluation error occurs, as a result of calling trigger(). With
268
   * errors suppressed this doesn't happen.
269
   *
270
   * @param bool $enable
271
   *   If FALSE, enable triggering of php errors when expression errors occurs.
272
   *   otherwise, suppress triggering the errors.
273
   *
274
   * @return bool
275
   *   The new (current) state of the flag.
276
   *
277
   * @see ctools_math_expr::trigger()
278
   */
279
  public function set_suppress_errors($enable) {
280
    return $this->suppress_errors = (bool) $enable;
281
  }
282
283
  /**
284
   * Backwards compatible wrapper for evaluate().
285
   *
286
   * @see ctools_math_expr::evaluate()
287
   */
288
  public function e($expr) {
289
    return $this->evaluate($expr);
290
  }
291
292
  /**
293
   * Evaluate the expression.
294
   *
295
   * @param string $expr
296
   *   The expression to evaluate.
297
   *
298
   * @return string|bool
299
   *   The result of the expression, or FALSE if an error occurred, or TRUE if
300
   *   an user-defined function was created.
301
   */
302
  public function evaluate($expr) {
303
    $this->last_error = NULL;
304
    $expr = trim($expr);
305
306
    // Strip possible semicolons at the end.
307
    if (substr($expr, -1, 1) == ';') {
308
      $expr = substr($expr, 0, -1);
309 85ad3d82 Assos Assos
    }
310
311 219d19c4 Assos Assos
    // Is it a variable assignment?
312
    if (preg_match('/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches)) {
313
314
      // Make sure we're not assigning to a constant.
315
      if (in_array($matches[1], $this->constvars)) {
316
        return $this->trigger("cannot assign to constant '$matches[1]'");
317
      }
318
319
      // Get the result and make sure it's good:
320
      if (($tmp = $this->pfx($this->nfx($matches[2]))) === FALSE) {
321
        return FALSE;
322
      }
323
      // If so, stick it in the variable array...
324
      $this->vars[$matches[1]] = $tmp;
325
      // ...and return the resulting value:
326
      return $this->vars[$matches[1]];
327
328 85ad3d82 Assos Assos
    }
329 219d19c4 Assos Assos
    // Is it a function assignment?
330
    elseif (preg_match('/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) {
331
      // Get the function name.
332
      $fnn = $matches[1];
333
      // Make sure it isn't built in:
334
      if (isset($this->funcs[$matches[1]])) {
335
        return $this->trigger("cannot redefine built-in function '$matches[1]()'");
336
      }
337
338
      // Get the arguments.
339
      $args = explode(",", preg_replace("/\s+/", "", $matches[2]));
340
      // See if it can be converted to postfix.
341
      $stack = $this->nfx($matches[3]);
342
      if ($stack === FALSE) {
343
        return FALSE;
344
      }
345
346
      // Freeze the state of the non-argument variables.
347
      for ($i = 0; $i < count($stack); $i++) {
348
        $token = $stack[$i];
349
        if (preg_match('/^[a-z]\w*$/', $token) and !in_array($token, $args)) {
350
          if (array_key_exists($token, $this->vars)) {
351
            $stack[$i] = $this->vars[$token];
352
          }
353
          else {
354
            return $this->trigger("undefined variable '$token' in function definition");
355
          }
356
        }
357
      }
358
      $this->userfuncs[$fnn] = array('args' => $args, 'func' => $stack);
359 85ad3d82 Assos Assos
360 219d19c4 Assos Assos
      return TRUE;
361 85ad3d82 Assos Assos
    }
362 219d19c4 Assos Assos
    else {
363
      // Straight up evaluation.
364
      return trim($this->pfx($this->nfx($expr)), '"');
365
    }
366
  }
367 85ad3d82 Assos Assos
368 219d19c4 Assos Assos
  /**
369
   * Fetch an array of variables used in the expression.
370
   *
371
   * @return array
372
   *   Array of name : value pairs, one for each variable defined.
373
   */
374
  public function vars() {
375
    $output = $this->vars;
376 85ad3d82 Assos Assos
377 219d19c4 Assos Assos
    // @todo: Is this supposed to remove all constants? we should remove all
378
    // those in $this->constvars!
379
    unset($output['pi']);
380
    unset($output['e']);
381 85ad3d82 Assos Assos
382 219d19c4 Assos Assos
    return $output;
383
  }
384 85ad3d82 Assos Assos
385 219d19c4 Assos Assos
  /**
386
   * Fetch all user defined functions in the expression.
387
   *
388
   * @return array
389
   *   Array of name : string pairs, one for each function defined. The string
390
   *   will be of the form fname(arg1,arg2). The function body is not returned.
391
   */
392
  public function funcs() {
393
    $output = array();
394
    foreach ($this->userfuncs as $fnn => $dat) {
395
      $output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
396
    }
397 85ad3d82 Assos Assos
398 219d19c4 Assos Assos
    return $output;
399
  }
400 85ad3d82 Assos Assos
401 219d19c4 Assos Assos
  /**
402
   * Convert infix to postfix notation.
403
   *
404
   * @param string $expr
405
   *   The expression to convert.
406
   *
407
   * @return array|bool
408
   *   The expression as an ordered list of postfix action tokens.
409
   */
410
  private function nfx($expr) {
411
412
    $index = 0;
413
    $stack = new ctools_math_expr_stack();
414
    // Postfix form of expression, to be passed to pfx().
415
    $output = array();
416
417
    // @todo: Because the expr can contain string operands, using strtolower here is a bug.
418
    $expr = trim(strtolower($expr));
419
420
    // We use this in syntax-checking the expression and determining when
421
    // '-' is a negation.
422
    $expecting_op = FALSE;
423
424
    while (TRUE) {
425
      $op = substr($expr, $index, 1);
426
      // Get the first character at the current index, and if the second
427
      // character is an =, add it to our op as well (accounts for <=).
428
      if (substr($expr, $index + 1, 1) === '=') {
429
        $op = substr($expr, $index, 2);
430
        $index++;
431
      }
432
433
      // Find out if we're currently at the beginning of a number/variable/
434
      // function/parenthesis/operand.
435
      $ex = preg_match('/^([a-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr($expr, $index), $match);
436
437
      // Is it a negation instead of a minus?
438
      if ($op === '-' and !$expecting_op) {
439
        // Put a negation on the stack.
440
        $stack->push('_');
441
        $index++;
442
      }
443
      // We have to explicitly deny this, because it's legal on the stack but
444
      // not in the input expression.
445
      elseif ($op == '_') {
446
        return $this->trigger("illegal character '_'");
447
448
      }
449
      // Are we putting an operator on the stack?
450
      elseif ((isset($this->ops[$op]) || $ex) && $expecting_op) {
451
        // Are we expecting an operator but have a num, var, func, or
452
        // open-paren?
453
        if ($ex) {
454
          $op = '*';
455
          // It's an implicit multiplication.
456
          $index--;
457
        }
458
        // Heart of the algorithm:
459
        while ($stack->count() > 0 &&
460
          ($o2 = $stack->last()) &&
461
          isset($this->ops[$o2]) &&
462
          (!empty($this->ops[$op]['right']) ?
463
            $this->ops[$op]['precedence'] < $this->ops[$o2]['precedence'] :
464
            $this->ops[$op]['precedence'] <= $this->ops[$o2]['precedence'])) {
465
466
          // Pop stuff off the stack into the output.
467
          $output[] = $stack->pop();
468
        }
469
        // Many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
470
        // finally put OUR operator onto the stack.
471
        $stack->push($op);
472
        $index++;
473
        $expecting_op = FALSE;
474
475
      }
476
      // Ready to close a parenthesis?
477
      elseif ($op === ')') {
478
479
        // Pop off the stack back to the last '('.
480
        while (($o2 = $stack->pop()) !== '(') {
481
          if (is_null($o2)) {
482
            return $this->trigger("unexpected ')'");
483
          }
484
          else {
485
            $output[] = $o2;
486
          }
487 85ad3d82 Assos Assos
        }
488
489 219d19c4 Assos Assos
        // Did we just close a function?
490
        if (preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches)) {
491
492
          // Get the function name.
493
          $fnn = $matches[1];
494
          // See how many arguments there were (cleverly stored on the stack,
495
          // thank you).
496
          $arg_count = $stack->pop();
497
          // Pop the function and push onto the output.
498
          $output[] = $stack->pop();
499
500
          // Check the argument count:
501
          if (isset($this->funcs[$fnn])) {
502
            $fdef = $this->funcs[$fnn];
503
            $max_arguments = isset($fdef['max arguments']) ? $fdef['max arguments'] : $fdef['arguments'];
504
            if ($arg_count > $max_arguments) {
505
              return $this->trigger("too many arguments ($arg_count given, $max_arguments expected)");
506 85ad3d82 Assos Assos
            }
507 219d19c4 Assos Assos
          }
508
          elseif (array_key_exists($fnn, $this->userfuncs)) {
509
            $fdef = $this->userfuncs[$fnn];
510
            if ($arg_count !== count($fdef['args'])) {
511
              return $this->trigger("wrong number of arguments ($arg_count given, " . count($fdef['args']) . ' expected)');
512
            }
513
          }
514
          else {
515
            // Did we somehow push a non-function on the stack? this should
516
            // never happen.
517
            return $this->trigger('internal error');
518
          }
519
        }
520
        $index++;
521
522
      }
523
      // Did we just finish a function argument?
524
      elseif ($op === ',' && $expecting_op) {
525
        $index++;
526
        $expecting_op = FALSE;
527
      }
528
      elseif ($op === '(' && !$expecting_op) {
529
        $stack->push('(');
530
        $index++;
531
532
      }
533
      elseif ($ex && !$expecting_op) {
534
        // Make sure there was a function.
535
        if (preg_match("/^([a-z]\w*)\($/", $stack->last(3), $matches)) {
536
          // Pop the argument expression stuff and push onto the output:
537
          while (($o2 = $stack->pop()) !== '(') {
538
            // Oops, never had a '('.
539
            if (is_null($o2)) {
540
              return $this->trigger("unexpected argument in $expr $o2");
541 85ad3d82 Assos Assos
            }
542 219d19c4 Assos Assos
            else {
543
              $output[] = $o2;
544 85ad3d82 Assos Assos
            }
545 219d19c4 Assos Assos
          }
546 85ad3d82 Assos Assos
547 219d19c4 Assos Assos
          // Increment the argument count.
548
          $stack->push($stack->pop() + 1);
549
          // Put the ( back on, we'll need to pop back to it again.
550
          $stack->push('(');
551
        }
552
553
        // Do we now have a function/variable/number?
554
        $expecting_op = TRUE;
555
        $val = $match[1];
556
        if (preg_match("/^([a-z]\w*)\($/", $val, $matches)) {
557
          // May be func, or variable w/ implicit multiplication against
558
          // parentheses...
559
          if (isset($this->funcs[$matches[1]]) or array_key_exists($matches[1], $this->userfuncs)) {
560
            $stack->push($val);
561
            $stack->push(0);
562
            $stack->push('(');
563
            $expecting_op = FALSE;
564
          }
565
          // it's a var w/ implicit multiplication.
566
          else {
567
            $val = $matches[1];
568
            $output[] = $val;
569
          }
570
        }
571
        // it's a plain old var or num.
572
        else {
573
          $output[] = $val;
574 85ad3d82 Assos Assos
        }
575 219d19c4 Assos Assos
        $index += strlen($val);
576
577
      }
578
      elseif ($op === ')') {
579
        // Miscellaneous error checking.
580
        return $this->trigger("unexpected ')'");
581
      }
582
      elseif (isset($this->ops[$op]) and !$expecting_op) {
583
        return $this->trigger("unexpected operator '$op'");
584
      }
585
      elseif ($op === '"') {
586
        // Fetch a quoted string.
587
        $string = substr($expr, $index);
588
        if (preg_match('/"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"/s', $string, $matches)) {
589
          $string = $matches[0];
590
          // Trim the quotes off:
591
          $output[] = $string;
592
          $index += strlen($string);
593
          $expecting_op = TRUE;
594 85ad3d82 Assos Assos
        }
595 219d19c4 Assos Assos
        else {
596
          return $this->trigger('open quote without close quote.');
597
        }
598
      }
599
      else {
600
        // I don't even want to know what you did to get here.
601
        return $this->trigger("an unexpected error occurred at $op");
602
      }
603
      if ($index === strlen($expr)) {
604
        if (isset($this->ops[$op])) {
605
          // Did we end with an operator? bad.
606
          return $this->trigger("operator '$op' lacks operand");
607
        }
608
        else {
609
          break;
610
        }
611
      }
612
613
      // Step the index past whitespace (pretty much turns whitespace into
614
      // implicit multiplication if no operator is there).
615
      while (substr($expr, $index, 1) === ' ') {
616
        $index++;
617
      }
618
    }
619
620
    // Pop everything off the stack and push onto output:
621
    while (!is_null($op = $stack->pop())) {
622
623
      // If there are (s on the stack, ()s were unbalanced.
624
      if ($op === '(') {
625
        return $this->trigger("expecting ')'");
626
      }
627
      $output[] = $op;
628
    }
629
630
    return $output;
631
  }
632
633
  /**
634
   * Evaluate a prefix-operator stack expression.
635
   *
636
   * @param array $tokens
637
   *   The array of token values to evaluate. A token is a string value
638
   *   representing either an operation to perform, a variable, or a value.
639
   *   Literal values are checked using is_numeric(), or a value that starts
640
   *   with a double-quote; functions and variables by existence in the
641
   *   appropriate tables.
642
   *   If FALSE is passed in the function terminates immediately, returning
643
   *   FALSE.
644
   * @param array $vars
645
   *   Additional variable values to use when evaluating the expression. These
646
   *   variables do not override internal variables with the same name.
647
   *
648
   * @return bool|mixed
649
   *   The expression's value, otherwise FALSE is returned if there is an error
650
   *   detected unless php error handling intervenes: see suppress_error.
651
   */
652
  public function pfx(array $tokens, array $vars = array()) {
653
654
    if ($tokens == FALSE) {
655
      return FALSE;
656 85ad3d82 Assos Assos
    }
657
658 219d19c4 Assos Assos
    $stack = new ctools_math_expr_stack();
659
660
    foreach ($tokens as $token) {
661
      // If the token is a binary operator, pop two values off the stack, do
662
      // the operation, and push the result back on again.
663
      if (in_array($token, $this->binaryops)) {
664
        if (is_null($op2 = $stack->pop())) {
665
          return $this->trigger('internal error');
666
        }
667
        if (is_null($op1 = $stack->pop())) {
668
          return $this->trigger('internal error');
669
        }
670
        switch ($token) {
671
          case '+':
672
            $stack->push($op1 + $op2);
673
            break;
674
675
          case '-':
676
            $stack->push($op1 - $op2);
677
            break;
678
679
          case '*':
680
            $stack->push($op1 * $op2);
681
            break;
682
683
          case '/':
684
            if ($op2 == 0) {
685
              return $this->trigger('division by zero');
686
            }
687
            $stack->push($op1 / $op2);
688
            break;
689
690
          case '^':
691
            $stack->push(pow($op1, $op2));
692
            break;
693
694
          case '==':
695
            $stack->push((int) ($op1 == $op2));
696
            break;
697
698
          case '!=':
699
            $stack->push((int) ($op1 != $op2));
700
            break;
701
702
          case '<=':
703
            $stack->push((int) ($op1 <= $op2));
704
            break;
705
706
          case '<':
707
            $stack->push((int) ($op1 < $op2));
708
            break;
709
710
          case '>=':
711
            $stack->push((int) ($op1 >= $op2));
712
            break;
713
714
          case '>':
715
            $stack->push((int) ($op1 > $op2));
716
            break;
717
        }
718
      }
719
      // If the token is a unary operator, pop one value off the stack, do the
720
      // operation, and push it back on again.
721
      elseif ($token === "_") {
722
        $stack->push(-1 * $stack->pop());
723
      }
724
      // If the token is a function, pop arguments off the stack, hand them to
725
      // the function, and push the result back on again.
726
      elseif (preg_match("/^([a-z]\w*)\($/", $token, $matches)) {
727
        $fnn = $matches[1];
728
729
        // Check for a built-in function.
730
        if (isset($this->funcs[$fnn])) {
731
          $args = array();
732
          // Collect all required args from the stack.
733
          for ($i = 0; $i < $this->funcs[$fnn]['arguments']; $i++) {
734
            if (is_null($op1 = $stack->pop())) {
735
              return $this->trigger("function $fnn missing argument $i");
736
            }
737
            $args[] = $op1;
738
          }
739
          // If func allows additional args, collect them too, stopping on a
740
          // NULL arg.
741
          if (!empty($this->funcs[$fnn]['max arguments'])) {
742
            for (; $i < $this->funcs[$fnn]['max arguments']; $i++) {
743
              $arg = $stack->pop();
744
              if (!isset($arg)) {
745
                break;
746
              }
747
              $args[] = $arg;
748
            }
749
          }
750
          $stack->push(
751
            call_user_func_array($this->funcs[$fnn]['function'], array_reverse($args))
752
          );
753
        }
754
755
        // Check for a user function.
756
        elseif (isset($fnn, $this->userfuncs)) {
757
          $args = array();
758
          for ($i = count($this->userfuncs[$fnn]['args']) - 1; $i >= 0; $i--) {
759
            $value = $stack->pop();
760
            $args[$this->userfuncs[$fnn]['args'][$i]] = $value;
761
            if (is_null($value)) {
762
              return $this->trigger('internal error');
763 85ad3d82 Assos Assos
            }
764 219d19c4 Assos Assos
          }
765
          // yay... recursion!!!!
766
          $stack->push($this->pfx($this->userfuncs[$fnn]['func'], $args));
767
        }
768
      }
769
      // If the token is a number or variable, push it on the stack.
770
      else {
771
        if (is_numeric($token) || $token[0] == '"') {
772
          $stack->push($token);
773
        }
774
        elseif (array_key_exists($token, $this->vars)) {
775
          $stack->push($this->vars[$token]);
776
        }
777
        elseif (array_key_exists($token, $vars)) {
778
          $stack->push($vars[$token]);
779 85ad3d82 Assos Assos
        }
780 219d19c4 Assos Assos
        else {
781
          return $this->trigger("undefined variable '$token'");
782
        }
783
      }
784
    }
785
    // When we're out of tokens, the stack should have a single element, the
786
    // final result:
787
    if ($stack->count() !== 1) {
788
      return $this->trigger('internal error');
789 85ad3d82 Assos Assos
    }
790
791 219d19c4 Assos Assos
    return $stack->pop();
792
  }
793
794
  /**
795
   * Trigger an error, but nicely, if need be.
796
   *
797
   * @param string $msg
798
   *   Message to add to trigger error.
799
   *
800
   * @return bool
801
   *   Can trigger error, then returns FALSE.
802
   */
803
  protected function trigger($msg) {
804
    $this->errors[] = $msg;
805
    $this->last_error = $msg;
806
    if (!$this->suppress_errors) {
807
      trigger_error($msg, E_USER_WARNING);
808 85ad3d82 Assos Assos
    }
809 219d19c4 Assos Assos
810
    return FALSE;
811
  }
812
813 85ad3d82 Assos Assos
}
814
815 219d19c4 Assos Assos
/**
816
 * Class implementing a simple stack structure, used by ctools_math_expr.
817
 */
818 85ad3d82 Assos Assos
class ctools_math_expr_stack {
819
820 219d19c4 Assos Assos
  /**
821
   * The stack.
822
   *
823
   * @var array
824
   */
825
  private $stack;
826
  /**
827
   * The stack pointer, points at the first empty space.
828
   *
829
   * @var int
830
   */
831
  private $count;
832 85ad3d82 Assos Assos
833 219d19c4 Assos Assos
  /**
834
   * Ctools_math_expr_stack constructor.
835
   */
836
  public function __construct() {
837
    $this->stack = array();
838
    $this->count = 0;
839
  }
840 85ad3d82 Assos Assos
841 219d19c4 Assos Assos
  /**
842
   * Push the value onto the stack.
843
   *
844
   * @param mixed $val
845
   */
846
  public function push($val) {
847
    $this->stack[$this->count] = $val;
848
    $this->count++;
849
  }
850
851
  /**
852
   * Remove the most recently pushed value and return it.
853
   *
854
   * @return mixed|null
855
   *   The most recently pushed value, or NULL if the stack was empty.
856
   */
857
  public function pop() {
858
    if ($this->count > 0) {
859
      $this->count--;
860 85ad3d82 Assos Assos
861 219d19c4 Assos Assos
      return $this->stack[$this->count];
862 85ad3d82 Assos Assos
    }
863 219d19c4 Assos Assos
    return NULL;
864
  }
865
866
  /**
867
   * "Peek" the stack, or Return a value from the stack without removing it.
868
   *
869
   * @param int $n
870
   *   Integer indicating which value to return. 1 is the topmost (i.e. the
871
   *   value that pop() would return), 2 indicates the next, 3 the third, etc.
872
   *
873
   * @return mixed|null
874
   *   A value pushed onto the stack at the nth position, or NULL if the stack
875
   *   was empty.
876
   */
877
  public function last($n = 1) {
878
    return !empty($this->stack[$this->count - $n]) ? $this->stack[$this->count - $n] : NULL;
879
  }
880
881
  /**
882
   * Return the number of items on the stack.
883
   *
884
   * @return int
885
   *   The number of items.
886
   */
887
  public function count() {
888
    return $this->count;
889
  }
890
891 85ad3d82 Assos Assos
}
892
893 219d19c4 Assos Assos
/**
894
 * Helper function for evaluating 'if' condition.
895
 *
896
 * @param int $expr
897
 *   The expression to test: if <> 0 then the $if expression is returned.
898
 * @param mixed $if
899
 *   The expression returned if the condition is true.
900
 * @param mixed $else
901
 *   Optional. The expression returned if the expression is false.
902
 *
903
 * @return mixed|null
904
 *   The result. NULL is returned when an If condition is False and no Else
905
 *   expression is provided.
906
 */
907
function ctools_math_expr_if($expr, $if, $else = NULL) {
908
  return $expr ? $if : $else;
909
}
910
911
/**
912
 * Remove any non-digits so that numbers like $4,511.23 still work.
913
 *
914
 * It might be good for those using the 12,345.67 format, but is awful for
915
 * those using other conventions.
916
 * Use of the php 'intl' module might work here, if the correct locale can be
917
 * derived, but that seems unlikely to be true in all cases.
918
 *
919
 * @todo: locale could break this since in some locales that's $4.512,33 so
920
 * there needs to be a way to detect that and make it work properly.
921
 *
922
 * @param mixed $arg
923
 *   A number string with possible leading chars.
924
 *
925
 * @return mixed
926
 *   Returns a number string.
927
 */
928
function ctools_math_expr_number($arg) {
929
  // @todo: A really bad idea: It might be good for those using the 12,345.67
930
  // format, but is awful for those using other conventions.
931
  // $arg = preg_replace("/[^0-9\.]/", '', $arg);.
932
  return $arg;
933
}