Projet

Général

Profil

Paste
Télécharger (17,7 ko) Statistiques
| Branche: | Révision:

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

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

    
87
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

    
102
  /**
103
   * ctools_math_expr constructor.
104
   */
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
    }
111

    
112
    function e($expr) {
113
        return $this->evaluate($expr);
114
    }
115

    
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
        }
154
    }
155

    
156
    function vars() {
157
        $output = $this->v;
158
        unset($output['pi']);
159
        unset($output['e']);
160
        return $output;
161
    }
162

    
163
    function funcs() {
164
        $output = array();
165
        foreach ($this->f as $fnn=>$dat)
166
            $output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
167
        return $output;
168
    }
169

    
170
    //===================== HERE BE INTERNAL METHODS ====================\\
171

    
172
    // Convert infix to postfix notation
173
    function nfx($expr) {
174

    
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));
179

    
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
183

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

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

    
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");
278
            }
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
                }
285
            }
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)
288
            }
289

    
290
        }
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;
294
        }
295
        return $output;
296
    }
297

    
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
                }
353
            }
354
        }
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();
358
    }
359

    
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;
365
    }
366
}
367

    
368
// for internal use
369
class ctools_math_expr_stack {
370

    
371
    var $stack = array();
372
    var $count = 0;
373

    
374
    function push($val) {
375
        $this->stack[$this->count] = $val;
376
        $this->count++;
377
    }
378

    
379
    function pop() {
380
        if ($this->count > 0) {
381
            $this->count--;
382
            return $this->stack[$this->count];
383
        }
384
        return null;
385
    }
386

    
387
    function last($n=1) {
388
        return !empty($this->stack[$this->count-$n]) ? $this->stack[$this->count-$n] : NULL;
389
    }
390
}
391