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
<?php
2

    
3
/**
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
class ctools_math_expr {
69

    
70
  /**
71
   * If TRUE do not call trigger_error on error other wise do.
72
   *
73
   * @var bool
74
   */
75
  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

    
141
  /**
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
    }
261
  }
262

    
263
  /**
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
    }
310

    
311
    // 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
    }
329
    // 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

    
360
      return TRUE;
361
    }
362
    else {
363
      // Straight up evaluation.
364
      return trim($this->pfx($this->nfx($expr)), '"');
365
    }
366
  }
367

    
368
  /**
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

    
377
    // @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

    
382
    return $output;
383
  }
384

    
385
  /**
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

    
398
    return $output;
399
  }
400

    
401
  /**
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
        }
488

    
489
        // 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
            }
507
          }
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
            }
542
            else {
543
              $output[] = $o2;
544
            }
545
          }
546

    
547
          // 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
        }
575
        $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
        }
595
        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
    }
657

    
658
    $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
            }
764
          }
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
        }
780
        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
    }
790

    
791
    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
    }
809

    
810
    return FALSE;
811
  }
812

    
813
}
814

    
815
/**
816
 * Class implementing a simple stack structure, used by ctools_math_expr.
817
 */
818
class ctools_math_expr_stack {
819

    
820
  /**
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

    
833
  /**
834
   * Ctools_math_expr_stack constructor.
835
   */
836
  public function __construct() {
837
    $this->stack = array();
838
    $this->count = 0;
839
  }
840

    
841
  /**
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

    
861
      return $this->stack[$this->count];
862
    }
863
    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
}
892

    
893
/**
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
}