Révision 219d19c4
Ajouté par Assos Assos il y a plus de 3 ans
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
Weekly update of contrib modules