ILIAS  release_8 Revision v8.24
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
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);
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());
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 {
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...
static getTableCache(int $table_id=null)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
substituteFieldValues(array $tokens)
Given an array of tokens, replace each token that is a placeholder (e.g.
isMathToken(string $token)
Check if a given token is a math expression.
calculateFunction(string $function, array $args=array())
Calculate a function with its arguments.
calculateFunctions(string $token)
Execute any math functions inside a token.
parseMath(array $tokens)
Parse a math expression.
parse()
Parse expression and return result.
__construct(string $expression, ilDclBaseRecordModel $record, ilDclBaseFieldModel $field)
calculate(string $operator, $left, $right)
getFunctionArgs(int $index, array $data)
Helper method to return the function and its arguments from a preg_replace_all $result array.
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)
static getMathTokens(string $math_expression)
Generate tokens for a math expression.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
global $DIC
Definition: feed.php:28
$index
Definition: metadata.php:145
$i
Definition: metadata.php:41
$lng
$token
Definition: xapitoken.php:70