ILIAS  release_5-2 Revision v5.2.25-18-g3f80b828510
class.ilDclExpressionParser.php
Go to the documentation of this file.
1<?php
2
10
11 const N_DECIMALS = 1;
12 const SCIENTIFIC_NOTATION_UPPER = 1000000000000;
13 const SCIENTIFIC_NOTATION_LOWER = 0.000000001;
17 protected $record;
21 protected $field;
25 protected $expression;
29 protected static $operators = array(
30 '+' => array( 'precedence' => 1 ),
31 '-' => array( 'precedence' => 1 ),
32 '*' => array( 'precedence' => 2 ),
33 '/' => array( 'precedence' => 2 ),
34 '^' => array( 'precedence' => 3 ),
35 );
39 protected static $cache_tokens = array();
43 protected static $cache_fields = array();
47 protected static $cache_math_tokens = array();
51 protected static $cache_math_function_tokens = array();
55 protected static $functions = array(
56 'SUM',
57 'AVERAGE',
58 'MIN',
59 'MAX',
60 );
61
62
69 $this->expression = $expression;
70 $this->record = $record;
71 $this->field = $field;
72 }
73
74
83 public function parse() {
84 if (isset(self::$cache_tokens[$this->field->getId()])) {
85 $tokens = self::$cache_tokens[$this->field->getId()];
86 } else {
87 $tokens = ilDclTokenizer::getTokens($this->expression);
88 self::$cache_tokens[$this->field->getId()] = $tokens;
89 }
90 // ilUtil::sendInfo( "<pre>" . print_r($tokens, 1) . "</pre>");
91 $parsed = '';
92 foreach ($tokens as $token) {
93 if (empty($token)) {
94 continue;
95 }
96 if ($this->isMathToken($token)) {
97 $token = $this->calculateFunctions($token);
98 $math_tokens = ilDclTokenizer::getMathTokens($token);
99 $value = $this->parseMath($this->substituteFieldValues($math_tokens));
100
101 $value = $this->formatScientific($value);
102
103 $parsed .= $value;
104 } else {
105 // Token is a string, either a field placeholder [[Field name]] or a string starting with "
106 if (strpos($token, '"') === 0) {
107 $parsed .= strip_tags(trim($token, '"'));
108 } elseif (strpos($token, '[[') === 0) {
109 $parsed .= trim(strip_tags($this->substituteFieldValue($token)));
110 } else {
111 throw new ilException("Unrecognized string token: '$token'");
112 }
113 }
114 }
115
116 return $parsed;
117 }
118
119
125 protected function formatScientific($value) {
126 if (abs($value) >= self::SCIENTIFIC_NOTATION_UPPER) {
127 return sprintf("%e", $value);
128 }
129 if (abs($value) <= self::SCIENTIFIC_NOTATION_LOWER && $value != 0 ) {
130 return sprintf("%e", $value);
131 }
132 if (is_float($value)) {
133 return $value;
134 }
135
136 return $value;
137 }
138
139
143 public static function getOperators() {
144 return self::$operators;
145 }
146
147
151 public static function getFunctions() {
152 return self::$functions;
153 }
154
155
163 protected function isMathToken($token) {
164 if (isset(self::$cache_math_tokens[$this->field->getId()][$token])) {
165 return self::$cache_math_tokens[$this->field->getId()][$token];
166 } else {
167 if (strpos($token, '"') === 0) {
168 return false;
169 }
170 $operators = array_keys(self::getOperators());
172 $result = (bool)preg_match('#(\\' . implode("|\\", $operators) . '|' . implode('|', $functions) . ')#', $token);
173 self::$cache_math_tokens[$this->field->getId()][$token] = $result;
174
175 return $result;
176 }
177 }
178
179
187 protected function calculateFunctions($token) {
188 if (isset(self::$cache_math_function_tokens[$this->field->getId()][$token])) {
189 $result = self::$cache_math_function_tokens[$this->field->getId()][$token];
190 if ($result === false) {
191 return $token;
192 }
193 } else {
194 $pattern = '#';
195 foreach (self::getFunctions() as $function) {
196 $pattern .= "($function)\\(([^)]*)\\)|";
197 }
198 if (!preg_match_all(rtrim($pattern, '|') . '#', $token, $result)) {
199 // No functions found inside token, just return token again
200 self::$cache_math_function_tokens[$this->field->getId()][$token] = false;
201
202 return $token;
203 }
204 }
205 // Function found inside token, calculate!
206 foreach ($result[0] as $k => $to_replace) {
207 $function_args = $this->getFunctionArgs($k, $result);
208 $function = $function_args['function'];
209 $args = $this->substituteFieldValues($function_args['args']);
210 $token = str_replace($to_replace, $this->calculateFunction($function, $args), $token);
211 }
212
213 return $token;
214 }
215
216
225 protected function getFunctionArgs($index, array $data) {
226 $return = array(
227 'function' => '',
228 'args' => array(),
229 );
230 for ($i = 1; $i < count($data); $i ++) {
231 $_data = $data[$i];
232 if ($_data[$index]) {
233 $function = $_data[$index];
234 $args = explode(';', $data[$i + 1][$index]);
235 $return['function'] = $function;
236 $return['args'] = $args;
237 break;
238 }
239 }
240
241 return $return;
242 }
243
244
252 protected function substituteFieldValues(array $tokens) {
253 $replaced = array();
254 foreach ($tokens as $token) {
255 if (strpos($token, '[[') === 0) {
256 $replaced[] = $this->substituteFieldValue($token);
257 } else {
258 $replaced[] = $token;
259 }
260 }
261
262 return $replaced;
263 }
264
265
274 protected function substituteFieldValue($placeholder) {
275 if (isset(self::$cache_fields[$placeholder])) {
276 $field = self::$cache_fields[$placeholder];
277 } else {
278 $table = ilDclCache::getTableCache($this->record->getTableId()); // TODO May need caching per table in future
279 $field_title = preg_replace('#^\[\[(.*)\]\]#', "$1", $placeholder);
280 $field = $table->getFieldByTitle($field_title);
281 if ($field === NULL) {
282 // Workaround for standardfields - title my be ID
283 $field = $table->getField($field_title);
284 if ($field === NULL) {
285 global $DIC;
286 $lng = $DIC['lng'];
290 $lng->loadLanguageModule('dcl');
291// throw new ilException("Field with title '$field_title' not found");
292 throw new ilException(sprintf($lng->txt('dcl_err_formula_field_not_found'), $field_title));
293 }
294 }
295 self::$cache_fields[$placeholder] = $field;
296 }
297
298 return $this->record->getRecordFieldHTML($field->getId());
299 }
300
301
310 protected function parseMath(array $tokens) {
312 $precedence = 0;
313 $stack = new ilDclStack();
314 $precedences = new ilDclStack();
315 $in_bracket = false;
316 foreach ($tokens as $token) {
317 if (empty($token) OR is_null($token)) {
318 $token = 0;
319 }
320 if (is_numeric($token) OR $token === '(') {
321 $stack->push($token);
322 if ($token === '(') {
323 $in_bracket = true;
324 }
325 } elseif (in_array($token, array_keys($operators))) {
326 $new_precedence = $operators[$token]['precedence'];
327 if ($new_precedence > $precedence || $in_bracket) {
328 // Precedence of operator is higher, push operator on stack
329 $stack->push($token);
330 $precedences->push($new_precedence);
331 $precedence = $new_precedence;
332 } else {
333 // Precedence is equal or lower, calculate result on stack
334 while ($new_precedence <= $precedence && $stack->count() > 1) {
335 $right = (float)$stack->pop();
336 $operator = $stack->pop();
337 $left = (float)$stack->pop();
338 $result = $this->calculate($operator, $left, $right);
339 $stack->push($result);
340 $precedence = $precedences->pop();
341 }
342 $stack->push($token);
343 $precedence = $new_precedence;
344 $precedences->push($new_precedence);
345 }
346 } elseif ($token === ')') {
347 // Need to calculate stack back to opening bracket
348 $_tokens = array();
349 $elem = $stack->pop();
350 while ($elem !== '(' && !$stack->isEmpty()) {
351 $_tokens[] = $elem;
352 $elem = $stack->pop();
353 }
354 // Get result within brackets recursive and push to stack
355 $stack->push($this->parseMath(array_reverse($_tokens)));
356 $in_bracket = false;
357 } else {
358 throw new ilException("Unrecognized token '$token'");
359 }
360 // $stack->debug();
361 }
362 // If one element is left on stack, we are done. Otherwise calculate
363 if ($stack->count() == 1) {
364 $result = $stack->pop();
365
366 return (ctype_digit((string)$result)) ? $result : number_format($result, self::N_DECIMALS, '.', "'");
367 } else {
368
369 while ($stack->count() >= 2) {
370 $right = $stack->pop();
371 $operator = $stack->pop();
372 $left = $stack->count() ? $stack->pop() : 0;
373 $stack->push($this->calculate($operator, $left, $right));
374 }
375 $result = $stack->pop();
376
377 return $result;
378 }
379 }
380
381
391 protected function calculateFunction($function, array $args = array()) {
392 switch ($function) {
393 case 'AVERAGE':
394 $count = count($args);
395
396 return ($count) ? array_sum($args) / $count : 0;
397 case 'SUM':
398 return array_sum($args);
399 case 'MIN':
400 return min($args);
401 case 'MAX':
402 return max($args);
403 default:
404 throw new ilException("Unrecognized function '$function'");
405 }
406 }
407
408
417 protected function calculate($operator, $left, $right) {
418 switch ($operator) {
419 case '+':
420 $result = $left + $right;
421 break;
422 case '-':
423 $result = $left - $right;
424 break;
425 case '*':
426 $result = $left * $right;
427 break;
428 case '/':
429 $result = ($right == 0) ? 0 : $left / $right;
430 break;
431 case '^':
432 $result = pow($left, $right);
433 break;
434 default:
435 throw new ilException("Unrecognized operator '$operator'");
436 }
437
438 return $result;
439 }
440}
441
442
443?>
sprintf('%.4f', $callTime)
$result
An exception for terminatinating execution or to throw for unit testing.
Class ilDclBaseFieldModel.
Class ilDclBaseRecordModel.
static getTableCache($table_id=0)
Class ilDclExpressionParser.
substituteFieldValues(array $tokens)
Given an array of tokens, replace each token that is a placeholder (e.g.
getFunctionArgs($index, array $data)
Helper method to return the function and its arguments from a preg_replace_all $result array.
calculateFunctions($token)
Execute any math functions inside a token.
parseMath(array $tokens)
Parse a math expression.
parse()
Parse expression and return result.
isMathToken($token)
Check if a given token is a math expression.
calculateFunction($function, array $args=array())
Calculate a function with its arguments.
__construct($expression, ilDclBaseRecordModel $record, ilDclBaseFieldModel $field)
calculate($operator, $left, $right)
Class ilDclStack.
static getMathTokens($math_expression)
Generate tokens for a math expression.
static getTokens($expression)
Base class for ILIAS Exception handling.
global $lng
Definition: privfeed.php:17
global $DIC