Projet

Général

Profil

Révision 219d19c4

Ajouté par Assos Assos il y a plus de 3 ans

Weekly update of contrib modules

Voir les différences:

drupal7/sites/all/modules/ctools/includes/math-expr.inc
1 1
<?php
2 2

  
3
/*
4
================================================================================
5

  
6
ctools_math_expr - PHP Class to safely evaluate math expressions
7
Copyright (C) 2005 Miles Kaufmann <http://www.twmagic.com/>
8

  
9
================================================================================
10

  
11
NAME
12
    ctools_math_expr - safely evaluate math expressions
13

  
14
SYNOPSIS
15
      include('ctools_math_expr.class.php');
16
      $m = new ctools_math_expr;
17
      // basic evaluation:
18
      $result = $m->evaluate('2+2');
19
      // supports: order of operation; parentheses; negation; built-in functions
20
      $result = $m->evaluate('-8(5/2)^2*(1-sqrt(4))-8');
21
      // create your own variables
22
      $m->evaluate('a = e^(ln(pi))');
23
      // or functions
24
      $m->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1');
25
      // and then use them
26
      $result = $m->evaluate('3*f(42,a)');
27

  
28
DESCRIPTION
29
    Use the ctools_math_expr class when you want to evaluate mathematical expressions
30
    from untrusted sources.  You can define your own variables and functions,
31
    which are stored in the object.  Try it, it's fun!
32

  
33
METHODS
34
    $m->evalute($expr)
35
        Evaluates the expression and returns the result.  If an error occurs,
36
        prints a warning and returns false.  If $expr is a function assignment,
37
        returns true on success.
38

  
39
    $m->e($expr)
40
        A synonym for $m->evaluate().
41

  
42
    $m->vars()
43
        Returns an associative array of all user-defined variables and values.
44

  
45
    $m->funcs()
46
        Returns an array of all user-defined functions.
47

  
48
PARAMETERS
49
    $m->suppress_errors
50
        Set to true to turn off warnings when evaluating expressions
51

  
52
    $m->last_error
53
        If the last evaluation failed, contains a string describing the error.
54
        (Useful when suppress_errors is on).
55

  
56
AUTHOR INFORMATION
57
    Copyright 2005, Miles Kaufmann.
58

  
59
LICENSE
60
    Redistribution and use in source and binary forms, with or without
61
    modification, are permitted provided that the following conditions are
62
    met:
63

  
64
    1   Redistributions of source code must retain the above copyright
65
        notice, this list of conditions and the following disclaimer.
66
    2.  Redistributions in binary form must reproduce the above copyright
67
        notice, this list of conditions and the following disclaimer in the
68
        documentation and/or other materials provided with the distribution.
69
    3.  The name of the author may not be used to endorse or promote
70
        products derived from this software without specific prior written
71
        permission.
72

  
73
    THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
74
    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
75
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
76
    DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
77
    INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
78
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
79
    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
80
    HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
81
    STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
82
    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
83
    POSSIBILITY OF SUCH DAMAGE.
84

  
85
*/
86

  
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
 */
87 68
class ctools_math_expr {
88
    var $suppress_errors = false;
89
    var $last_error = null;
90

  
91
    var $v = array('e'=>2.71,'pi'=>3.14); // variables (and constants)
92
    var $f = array(); // user-defined functions
93
    var $vb = array('e', 'pi'); // constants
94
    var $fb = array(  // built-in functions
95
        'sin','sinh','arcsin','asin','arcsinh','asinh',
96
        'cos','cosh','arccos','acos','arccosh','acosh',
97
        'tan','tanh','arctan','atan','arctanh','atanh',
98
        'pow', 'exp',
99
        'sqrt','abs','ln','log',
100
        'time', 'ceil', 'floor', 'min', 'max', 'round');
101 69

  
102 70
  /**
103
   * ctools_math_expr constructor.
71
   * If TRUE do not call trigger_error on error other wise do.
72
   *
73
   * @var bool
104 74
   */
105
  function __construct() {
106
        // make the variables a little more accurate
107
        $this->v['pi'] = pi();
108
        $this->v['e'] = exp(1);
109
        drupal_alter('ctools_math_expression_functions', $this->fb);
110
    }
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;
111 140

  
112
    function e($expr) {
113
        return $this->evaluate($expr);
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
      );
114 260
    }
261
  }
115 262

  
116
    function evaluate($expr) {
117
        $this->last_error = null;
118
        $expr = trim($expr);
119
        if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end
120
        //===============
121
        // is it a variable assignment?
122
        if (preg_match('/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches)) {
123
            if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant
124
                return $this->trigger("cannot assign to constant '$matches[1]'");
125
            }
126
            if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) return false; // get the result and make sure it's good
127
            $this->v[$matches[1]] = $tmp; // if so, stick it in the variable array
128
            return $this->v[$matches[1]]; // and return the resulting value
129
        //===============
130
        // is it a function assignment?
131
        } elseif (preg_match('/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) {
132
            $fnn = $matches[1]; // get the function name
133
            if (in_array($matches[1], $this->fb)) { // make sure it isn't built in
134
                return $this->trigger("cannot redefine built-in function '$matches[1]()'");
135
            }
136
            $args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments
137
            if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix
138
            for ($i = 0; $i<count($stack); $i++) { // freeze the state of the non-argument variables
139
                $token = $stack[$i];
140
                if (preg_match('/^[a-z]\w*$/', $token) and !in_array($token, $args)) {
141
                    if (array_key_exists($token, $this->v)) {
142
                        $stack[$i] = $this->v[$token];
143
                    } else {
144
                        return $this->trigger("undefined variable '$token' in function definition");
145
                    }
146
                }
147
            }
148
            $this->f[$fnn] = array('args'=>$args, 'func'=>$stack);
149
            return true;
150
        //===============
151
        } else {
152
            return $this->pfx($this->nfx($expr)); // straight up evaluation, woo
153
        }
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);
154 309
    }
155 310

  
156
    function vars() {
157
        $output = $this->v;
158
        unset($output['pi']);
159
        unset($output['e']);
160
        return $output;
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

  
161 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);
162 359

  
163
    function funcs() {
164
        $output = array();
165
        foreach ($this->f as $fnn=>$dat)
166
            $output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
167
        return $output;
360
      return TRUE;
168 361
    }
362
    else {
363
      // Straight up evaluation.
364
      return trim($this->pfx($this->nfx($expr)), '"');
365
    }
366
  }
169 367

  
170
    //===================== HERE BE INTERNAL METHODS ====================\\
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;
171 376

  
172
    // Convert infix to postfix notation
173
    function nfx($expr) {
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']);
174 381

  
175
        $index = 0;
176
        $stack = new ctools_math_expr_stack;
177
        $output = array(); // postfix form of expression, to be passed to pfx()
178
        $expr = trim(strtolower($expr));
382
    return $output;
383
  }
179 384

  
180
        $ops   = array('+', '-', '*', '/', '^', '_');
181
        $ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator?
182
        $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2); // operator precedence
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
    }
183 397

  
184
        $expecting_op = false; // we use this in syntax-checking the expression
185
                               // and determining when a - is a negation
398
    return $output;
399
  }
186 400

  
187
        if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good
188
            return $this->trigger("illegal character '{$matches[0]}'");
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
          }
189 487
        }
190 488

  
191
        while(1) { // 1 Infinite Loop ;)
192
            $op = substr($expr, $index, 1); // get the first character at the current index
193
            // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
194
            $ex = preg_match('/^([a-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr($expr, $index), $match);
195
            //===============
196
            if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
197
                $stack->push('_'); // put a negation on the stack
198
                $index++;
199
            } elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack
200
                return $this->trigger("illegal character '_'"); // but not in the input expression
201
            //===============
202
            } elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack?
203
                if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis?
204
                    $op = '*'; $index--; // it's an implicit multiplication
205
                }
206
                // heart of the algorithm:
207
                while($stack->count > 0 and ($o2 = $stack->last()) and in_array($o2, $ops) and ($ops_r[$op] ? $ops_p[$op] < $ops_p[$o2] : $ops_p[$op] <= $ops_p[$o2])) {
208
                    $output[] = $stack->pop(); // pop stuff off the stack into the output
209
                }
210
                // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
211
                $stack->push($op); // finally put OUR operator onto the stack
212
                $index++;
213
                $expecting_op = false;
214
            //===============
215
            } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
216
                while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last (
217
                    if (is_null($o2)) return $this->trigger("unexpected ')'");
218
                    else $output[] = $o2;
219
                }
220
                if (preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches)) { // did we just close a function?
221
                    $fnn = $matches[1]; // get the function name
222
                    $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
223
                    $output[] = $stack->pop(); // pop the function and push onto the output
224
                    if (in_array($fnn, $this->fb)) { // check the argument count
225
                        if($arg_count > 1)
226
                            return $this->trigger("too many arguments ($arg_count given, 1 expected)");
227
                    } elseif (array_key_exists($fnn, $this->f)) {
228
                        if ($arg_count != count($this->f[$fnn]['args']))
229
                            return $this->trigger("wrong number of arguments ($arg_count given, " . count($this->f[$fnn]['args']) . " expected)");
230
                    } else { // did we somehow push a non-function on the stack? this should never happen
231
                        return $this->trigger("internal error");
232
                    }
233
                }
234
                $index++;
235
            //===============
236
            } elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
237
                while (($o2 = $stack->pop()) != '(') {
238
                    if (is_null($o2)) return $this->trigger("unexpected ','"); // oops, never had a (
239
                    else $output[] = $o2; // pop the argument expression stuff and push onto the output
240
                }
241
                // make sure there was a function
242
                if (!preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches))
243
                    return $this->trigger("unexpected ','");
244
                $stack->push($stack->pop()+1); // increment the argument count
245
                $stack->push('('); // put the ( back on, we'll need to pop back to it again
246
                $index++;
247
                $expecting_op = false;
248
            //===============
249
            } elseif ($op == '(' and !$expecting_op) {
250
                $stack->push('('); // that was easy
251
                $index++;
252
                $allow_neg = true;
253
            //===============
254
            } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
255
                $expecting_op = true;
256
                $val = $match[1];
257
                if (preg_match("/^([a-z]\w*)\($/", $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
258
                    if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f)) { // it's a func
259
                        $stack->push($val);
260
                        $stack->push(1);
261
                        $stack->push('(');
262
                        $expecting_op = false;
263
                    } else { // it's a var w/ implicit multiplication
264
                        $val = $matches[1];
265
                        $output[] = $val;
266
                    }
267
                } else { // it's a plain old var or num
268
                    $output[] = $val;
269
                }
270
                $index += strlen($val);
271
            //===============
272
            } elseif ($op == ')') { // miscellaneous error checking
273
                return $this->trigger("unexpected ')'");
274
            } elseif (in_array($op, $ops) and !$expecting_op) {
275
                return $this->trigger("unexpected operator '$op'");
276
            } else { // I don't even want to know what you did to get here
277
                return $this->trigger("an unexpected error occurred");
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)");
278 506
            }
279
            if ($index == strlen($expr)) {
280
                if (in_array($op, $ops)) { // did we end with an operator? bad.
281
                    return $this->trigger("operator '$op' lacks operand");
282
                } else {
283
                    break;
284
                }
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");
285 541
            }
286
            while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace
287
                $index++;                             // into implicit multiplication if no operator is there)
542
            else {
543
              $output[] = $o2;
288 544
            }
545
          }
289 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;
290 574
        }
291
        while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output
292
            if ($op == '(') return $this->trigger("expecting ')'"); // if there are (s on the stack, ()s were unbalanced
293
            $output[] = $op;
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;
294 594
        }
295
        return $output;
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;
296 656
    }
297 657

  
298
    // evaluate postfix notation
299
    function pfx($tokens, $vars = array()) {
300

  
301
        if ($tokens == false) return false;
302

  
303
        $stack = new ctools_math_expr_stack;
304

  
305
        foreach ($tokens as $token) { // nice and easy
306
            // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
307
            if (in_array($token, array('+', '-', '*', '/', '^'))) {
308
                if (is_null($op2 = $stack->pop())) return $this->trigger("internal error");
309
                if (is_null($op1 = $stack->pop())) return $this->trigger("internal error");
310
                switch ($token) {
311
                    case '+':
312
                        $stack->push($op1+$op2); break;
313
                    case '-':
314
                        $stack->push($op1-$op2); break;
315
                    case '*':
316
                        $stack->push($op1*$op2); break;
317
                    case '/':
318
                        if ($op2 == 0) return $this->trigger("division by zero");
319
                        $stack->push($op1/$op2); break;
320
                    case '^':
321
                        $stack->push(pow($op1, $op2)); break;
322
                }
323
            // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
324
            } elseif ($token == "_") {
325
                $stack->push(-1*$stack->pop());
326
            // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
327
            } elseif (preg_match("/^([a-z]\w*)\($/", $token, $matches)) { // it's a function!
328
                $fnn = $matches[1];
329
                if (in_array($fnn, $this->fb)) { // built-in function:
330
                    if (is_null($op1 = $stack->pop())) return $this->trigger("internal error");
331
                    $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
332
                    if ($fnn == 'ln') $fnn = 'log';
333
                    eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval()
334
                } elseif (array_key_exists($fnn, $this->f)) { // user function
335
                    // get args
336
                    $args = array();
337
                    for ($i = count($this->f[$fnn]['args'])-1; $i >= 0; $i--) {
338
                        if (is_null($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) return $this->trigger("internal error");
339
                    }
340
                    $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
341
                }
342
            // if the token is a number or variable, push it on the stack
343
            } else {
344
                if (is_numeric($token)) {
345
                    $stack->push($token);
346
                } elseif (array_key_exists($token, $this->v)) {
347
                    $stack->push($this->v[$token]);
348
                } elseif (array_key_exists($token, $vars)) {
349
                    $stack->push($vars[$token]);
350
                } else {
351
                    return $this->trigger("undefined variable '$token'");
352
                }
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');
353 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]);
354 779
        }
355
        // when we're out of tokens, the stack should have a single element, the final result
356
        if ($stack->count != 1) return $this->trigger("internal error");
357
        return $stack->pop();
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');
358 789
    }
359 790

  
360
    // trigger an error, but nicely, if need be
361
    function trigger($msg) {
362
        $this->last_error = $msg;
363
        if (!$this->suppress_errors) trigger_error($msg, E_USER_WARNING);
364
        return false;
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);
365 808
    }
809

  
810
    return FALSE;
811
  }
812

  
366 813
}
367 814

  
368
// for internal use
815
/**
816
 * Class implementing a simple stack structure, used by ctools_math_expr.
817
 */
369 818
class ctools_math_expr_stack {
370 819

  
371
    var $stack = array();
372
    var $count = 0;
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;
373 832

  
374
    function push($val) {
375
        $this->stack[$this->count] = $val;
376
        $this->count++;
377
    }
833
  /**
834
   * Ctools_math_expr_stack constructor.
835
   */
836
  public function __construct() {
837
    $this->stack = array();
838
    $this->count = 0;
839
  }
378 840

  
379
    function pop() {
380
        if ($this->count > 0) {
381
            $this->count--;
382
            return $this->stack[$this->count];
383
        }
384
        return null;
385
    }
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--;
386 860

  
387
    function last($n=1) {
388
        return !empty($this->stack[$this->count-$n]) ? $this->stack[$this->count-$n] : NULL;
861
      return $this->stack[$this->count];
389 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

  
390 891
}
391 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
}

Formats disponibles : Unified diff