ILIAS  release_8 Revision v8.19
All Data Structures Namespaces Files Functions Variables Modules Pages
class.ilDclExpressionParser.php
Go to the documentation of this file.
1 <?php
2 
20 {
21  public const N_DECIMALS = 1;
22  public const SCIENTIFIC_NOTATION_UPPER = 1000000000000;
23  public const SCIENTIFIC_NOTATION_LOWER = 0.000000001;
24 
27  protected string $expression;
28  protected static array $operators
29  = array(
30  '+' => array('precedence' => 1),
31  '-' => array('precedence' => 1),
32  '*' => array('precedence' => 2),
33  '/' => array('precedence' => 2),
34  '^' => array('precedence' => 3),
35  );
36  protected static array $cache_tokens = array();
37  protected static array $cache_fields = array();
38  protected static array $cache_math_tokens = array();
39  protected static array $cache_math_function_tokens = array();
40  protected static array $functions
41  = array(
42  'SUM',
43  'AVERAGE',
44  'MIN',
45  'MAX',
46  );
47 
48  public function __construct(string $expression, ilDclBaseRecordModel $record, ilDclBaseFieldModel $field)
49  {
50  $this->expression = $expression;
51  $this->record = $record;
52  $this->field = $field;
53  }
54 
61  public function parse(): string
62  {
63  if (isset(self::$cache_tokens[$this->field->getId()])) {
64  $tokens = self::$cache_tokens[$this->field->getId()];
65  } else {
66  $tokens = ilDclTokenizer::getTokens($this->expression);
67  self::$cache_tokens[$this->field->getId()] = $tokens;
68  }
69  $parsed = '';
70  foreach ($tokens as $token) {
71  if (empty($token)) {
72  continue;
73  }
74  if ($this->isMathToken($token)) {
75  $token = $this->calculateFunctions($token);
76  $math_tokens = ilDclTokenizer::getMathTokens($token);
77  $value = $this->parseMath($this->substituteFieldValues($math_tokens));
78 
79  $value = $this->formatScientific($value);
80 
81  $parsed .= $value;
82  } else {
83  // Token is a string, either a field placeholder [[Field name]] or a string starting with "
84  if (strpos($token, '"') === 0) {
85  $parsed .= strip_tags(trim($token, '"'));
86  } elseif (strpos($token, '[[') === 0) {
87  $parsed .= trim(strip_tags($this->substituteFieldValue($token)));
88  } else {
89  throw new ilException("Unrecognized string token: '$token'");
90  }
91  }
92  }
93 
94  return $parsed;
95  }
96 
101  protected function formatScientific($value)
102  {
103  if (abs($value) >= self::SCIENTIFIC_NOTATION_UPPER) {
104  return sprintf("%e", $value);
105  }
106  if (abs($value) <= self::SCIENTIFIC_NOTATION_LOWER && $value != 0) {
107  return sprintf("%e", $value);
108  }
109  return $value;
110  }
111 
112  public static function getOperators(): array
113  {
114  return self::$operators;
115  }
116 
117  public static function getFunctions(): array
118  {
119  return self::$functions;
120  }
121 
125  protected function isMathToken(string $token): bool
126  {
127  if (isset(self::$cache_math_tokens[$this->field->getId()][$token])) {
128  return self::$cache_math_tokens[$this->field->getId()][$token];
129  } else {
130  if (strpos($token, '"') === 0) {
131  return false;
132  }
133  $operators = array_keys(self::getOperators());
134  $functions = self::getFunctions();
135  $result = (bool) preg_match(
136  '#(\\' . implode("|\\", $operators) . '|' . implode('|', $functions) . ')#',
137  $token
138  );
139  self::$cache_math_tokens[$this->field->getId()][$token] = $result;
140 
141  return $result;
142  }
143  }
144 
148  protected function calculateFunctions(string $token): string
149  {
150  if (isset(self::$cache_math_function_tokens[$this->field->getId()][$token])) {
151  $result = self::$cache_math_function_tokens[$this->field->getId()][$token];
152  if ($result === false) {
153  return $token;
154  }
155  } else {
156  $pattern = '#';
157  foreach (self::getFunctions() as $function) {
158  $pattern .= "($function)\\(([^)]*)\\)|";
159  }
160  if (!preg_match_all(rtrim($pattern, '|') . '#', $token, $result)) {
161  // No functions found inside token, just return token again
162  self::$cache_math_function_tokens[$this->field->getId()][$token] = false;
163 
164  return $token;
165  }
166  }
167  // Function found inside token, calculate!
168  foreach ($result[0] as $k => $to_replace) {
169  $function_args = $this->getFunctionArgs($k, $result);
170  $function = $function_args['function'];
171  $args = $this->substituteFieldValues($function_args['args']);
172  $token = str_replace($to_replace, $this->calculateFunction($function, $args), $token);
173  }
174 
175  return $token;
176  }
177 
181  protected function getFunctionArgs(int $index, array $data): array
182  {
183  $return = array(
184  'function' => '',
185  'args' => array(),
186  );
187  for ($i = 1; $i < count($data); $i++) {
188  $_data = $data[$i];
189  if ($_data[$index]) {
190  $function = $_data[$index];
191  $args = explode(';', $data[$i + 1][$index]);
192  $return['function'] = $function;
193  $return['args'] = $args;
194  break;
195  }
196  }
197 
198  return $return;
199  }
200 
204  protected function substituteFieldValues(array $tokens): array
205  {
206  $replaced = array();
207  foreach ($tokens as $token) {
208  if (strpos($token, '[[') === 0) {
209  $replaced[] = $this->substituteFieldValue($token);
210  } else {
211  $replaced[] = $token;
212  }
213  }
214 
215  return $replaced;
216  }
217 
222  protected function substituteFieldValue(string $placeholder): string
223  {
224  if (isset(self::$cache_fields[$placeholder])) {
225  $field = self::$cache_fields[$placeholder];
226  } else {
227  $table = ilDclCache::getTableCache($this->record->getTableId()); // TODO May need caching per table in future
228  $field_title = preg_replace('#^\[\[(.*)\]\]#', "$1", $placeholder);
229  $field = $table->getFieldByTitle($field_title);
230  if ($field === null) {
231  // Workaround for standardfields - title my be ID
232  $field = $table->getField($field_title);
233  if ($field === null) {
234  global $DIC;
235  $lng = $DIC['lng'];
239  $lng->loadLanguageModule('dcl');
240  // throw new ilException("Field with title '$field_title' not found");
241  throw new ilException(sprintf($lng->txt('dcl_err_formula_field_not_found'), $field_title));
242  }
243  }
244  self::$cache_fields[$placeholder] = $field;
245  }
246 
247  return $this->record->getRecordFieldFormulaValue($field->getId());
248  }
249 
254  protected function parseMath(array $tokens): ?string
255  {
256  $operators = self::$operators;
257  $precedence = 0;
258  $stack = new ilDclStack();
259  $precedences = new ilDclStack();
260  $in_bracket = false;
261  foreach ($tokens as $token) {
262  if (empty($token) or is_null($token)) {
263  $token = 0;
264  }
265  if (is_numeric($token) or $token === '(') {
266  $stack->push($token);
267  if ($token === '(') {
268  $in_bracket = true;
269  }
270  } elseif (in_array($token, array_keys($operators))) {
271  $new_precedence = $operators[$token]['precedence'];
272  if ($new_precedence > $precedence || $in_bracket) {
273  // Precedence of operator is higher, push operator on stack
274  $stack->push($token);
275  $precedences->push($new_precedence);
276  $precedence = $new_precedence;
277  } else {
278  // Precedence is equal or lower, calculate result on stack
279  while ($new_precedence <= $precedence && $stack->count() > 1) {
280  $right = (float) $stack->pop();
281  $operator = $stack->pop();
282  $left = (float) $stack->pop();
283  $result = $this->calculate($operator, $left, $right);
284  $stack->push($result);
285  $precedence = $precedences->pop();
286  }
287  $stack->push($token);
288  $precedence = $new_precedence;
289  $precedences->push($new_precedence);
290  }
291  } elseif ($token === ')') {
292  // Need to calculate stack back to opening bracket
293  $_tokens = array();
294  $elem = $stack->pop();
295  while ($elem !== '(' && !$stack->isEmpty()) {
296  $_tokens[] = $elem;
297  $elem = $stack->pop();
298  }
299  // Get result within brackets recursive and push to stack
300  $stack->push($this->parseMath(array_reverse($_tokens)));
301  $in_bracket = false;
302  } else {
303  throw new ilException("Unrecognized token '$token'");
304  }
305  // $stack->debug();
306  }
307  // If one element is left on stack, we are done. Otherwise calculate
308  if ($stack->count() == 1) {
309  $result = $stack->pop();
310 
311  return (ctype_digit((string) $result)) ? $result : number_format($result, self::N_DECIMALS, '.', "'");
312  } else {
313  while ($stack->count() >= 2) {
314  $right = $stack->pop();
315  $operator = $stack->pop();
316  $left = $stack->count() ? $stack->pop() : 0;
317  $stack->push($this->calculate($operator, $left, $right));
318  }
319  $result = $stack->pop();
320 
321  return $result;
322  }
323  }
324 
330  protected function calculateFunction(string $function, array $args = array())
331  {
332  switch ($function) {
333  case 'AVERAGE':
334  $count = count($args);
335 
336  return ($count) ? array_sum($args) / $count : 0;
337  case 'SUM':
338  return array_sum($args);
339  case 'MIN':
340  return min($args);
341  case 'MAX':
342  return max($args);
343  default:
344  throw new ilException("Unrecognized function '$function'");
345  }
346  }
347 
355  protected function calculate(string $operator, $left, $right)
356  {
357  switch ($operator) {
358  case '+':
359  $result = $left + $right;
360  break;
361  case '-':
362  $result = $left - $right;
363  break;
364  case '*':
365  $result = $left * $right;
366  break;
367  case '/':
368  $result = ($right == 0) ? 0 : $left / $right;
369  break;
370  case '^':
371  $result = pow($left, $right);
372  break;
373  default:
374  throw new ilException("Unrecognized operator '$operator'");
375  }
376 
377  return $result;
378  }
379 }
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
parse()
Parse expression and return result.
$lng
substituteFieldValues(array $tokens)
Given an array of tokens, replace each token that is a placeholder (e.g.
calculateFunction(string $function, array $args=array())
Calculate a function with its arguments.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static getTokens(string $expression)
Split expression by & (ignore escaped &-symbols with backslash)
$index
Definition: metadata.php:145
parseMath(array $tokens)
Parse a math expression.
global $DIC
Definition: feed.php:28
static getTableCache(int $table_id=null)
getFunctionArgs(int $index, array $data)
Helper method to return the function and its arguments from a preg_replace_all $result array...
$token
Definition: xapitoken.php:70
static getMathTokens(string $math_expression)
Generate tokens for a math expression.
calculateFunctions(string $token)
Execute any math functions inside a token.
calculate(string $operator, $left, $right)
__construct(string $expression, ilDclBaseRecordModel $record, ilDclBaseFieldModel $field)
isMathToken(string $token)
Check if a given token is a math expression.
$i
Definition: metadata.php:41