ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
MathResultResolver.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
23 use ilDclDatatype;
34 
36 {
37  private \ilLanguage $lng;
38 
39  public function __construct(
40  private FieldSubstitution $substitution,
41  private Tokenizer $tokenizer,
42  ) {
43  }
44 
45  public function resolve(Token $token): Result
46  {
47  $calculated_token = $this->calculateFunctions($token);
48  $math_tokens = $this->tokenizer->tokenizeMath($calculated_token->getValue());
49 
50  $substituted = $this->substitution->substituteFieldValues($math_tokens);
51  [$result, $last_operator] = $this->parseMath($substituted);
52 
53  $from_function = $calculated_token->getFromFunction();
54 
55  if ($this->hasDateFieldsInMathTokens($math_tokens, $token)) {
56  return new DateResult(
57  (string) $result,
58  $from_function,
59  $last_operator
60  );
61  }
62 
63  return new IntegerResult(
64  (string) $result,
65  $from_function
66  );
67  }
68 
69  private function hasDateFieldsInMathTokens(array $math_tokens, Token $original_token): bool
70  {
71  foreach ($math_tokens as $math_token) {
72  if (str_starts_with($math_token->getValue(), Tokenizer::FIELD_OPENER)) {
73  $field = $this->substitution->getFieldFromPlaceholder($math_token->getValue());
74  if ($field->getDatatypeId() === ilDclDatatype::INPUTFORMAT_DATE) {
75  return true;
76  }
77  }
78  }
79 
80  // fallback to original token
81  $tokens = $this->tokenizer->tokenize($original_token->getValue());
82  foreach ($tokens as $token) {
83  // find placeholders in token-values using regex, placeholders start with [[ and end with ]], there can be multiple
84  if (preg_match_all('/\[\[(.*?)\]\]/', $token->getValue(), $matches)) {
85  foreach ($matches[1] as $match) {
86  $field = $this->substitution->getFieldFromPlaceholder($match);
87  if ($field->getDatatypeId() === ilDclDatatype::INPUTFORMAT_DATE) {
88  return true;
89  }
90  }
91  }
92  }
93 
94  return false;
95  }
96 
97  protected function parseMath(array $tokens): array
98  {
99  $operators = array_map(
100  static fn(Operators $operator): string => $operator->value,
102  );
103  $precedence = 0;
104  $stack = new Stack();
105  $precedences = new Stack();
106  $in_bracket = false;
107  foreach ($tokens as $token) {
108  if (is_string($token)) {
109  $token = new MathToken($token);
110  }
111 
112  // we use the tokens value
113  $token = $token->getValue() === '' ? '0' : $token->getValue();
114 
115  if (is_numeric($token) || $token === '(') {
116  $stack->push($token);
117  if ($token === '(') {
118  $in_bracket = true;
119  }
120  } elseif (in_array($token, $operators)) {
121  $last_operator = Operators::from($token);
122  $new_precedence = $last_operator->getPrecedence();
123  if ($new_precedence > $precedence || $in_bracket) {
124  // Precedence of operator is higher, push operator on stack
125  $stack->push($token);
126  $precedences->push($new_precedence);
127  $precedence = $new_precedence;
128  } else {
129  // Precedence is equal or lower, calculate result on stack
130  while ($new_precedence <= $precedence && $stack->count() > 1) {
131  $right = (float) $stack->pop();
132  $operator = $stack->pop();
133  $left = (float) $stack->pop();
134  $result = $this->calculate($operator, $left, $right);
135  $stack->push($result);
136  $precedence = $precedences->pop();
137  }
138  $stack->push($token);
139  $precedence = $new_precedence;
140  $precedences->push($new_precedence);
141  }
142  } elseif ($token === ')') {
143  // Need to calculate stack back to opening bracket
144  $_tokens = [];
145  $elem = $stack->pop();
146  while ($elem !== '(' && !$stack->isEmpty()) {
147  $_tokens[] = $elem;
148  $elem = $stack->pop();
149  }
150  // Get result within brackets recursive and push to stack
151  $stack->push($this->parseMath(array_reverse($_tokens))[0]);
152  $in_bracket = false;
153  } else {
154  throw new \ilException("Unrecognized token '$token'");
155  }
156  }
157  // If one element is left on stack, we are done. Otherwise calculate
158  if ($stack->count() === 1) {
159  $value = (int) round((float) $stack->pop());
160  return [$value, $last_operator ?? null];
161  }
162 
163  while ($stack->count() >= 2) {
164  $right = $stack->pop();
165  $operator = $stack->pop();
166  $left = $stack->count() ? $stack->pop() : 0;
167  $stack->push($this->calculate($operator, $left, $right));
168  }
169  return [$stack->pop(), $last_operator];
170  }
171 
172  protected function calculateFunctions(Token $token): Token
173  {
174  $pattern = '#';
175  $functions = array_map(
176  static fn(Functions $function): string => $function->value,
178  );
179 
180  foreach ($functions as $function) {
181  $pattern .= "($function)\\(([^)]*)\\)|";
182  }
183  if (!preg_match_all(rtrim($pattern, '|') . '#', $token->getValue(), $result)) {
184  // No functions found inside token, just return token again
185  return $token;
186  }
187  $token_value = $token->getValue();
188  // Function found inside token, calculate!
189  foreach ($result[0] as $k => $to_replace) {
190  $function_args = $this->getFunctionArgs($k, $result);
191  $function = $function_args['function'];
192  $args = $this->substitution->substituteFieldValues($function_args['args']);
193  $token_value = str_replace($to_replace, (string) $this->calculateFunction($function, $args), $token_value);
194  }
195 
196  return new MathToken(
197  $token_value,
198  Functions::tryFrom($function ?? null)
199  );
200  }
201 
205  protected function getFunctionArgs(int $index, array $data): array
206  {
207  $return = [
208  'function' => '',
209  'args' => [],
210  ];
211  for ($i = 1; $i < count($data); $i++) {
212  $_data = $data[$i];
213  if ($_data[$index]) {
214  $function = $_data[$index];
215  $args = explode(';', $data[$i + 1][$index]);
216  $return['function'] = $function;
217  $return['args'] = $args;
218  break;
219  }
220  }
221 
222  return $return;
223  }
224 
225  protected function calculateFunction(string $function, array $args = [])
226  {
227  $args = array_map(function (Token $arg) {
228  return (float) $arg->getValue();
229  }, $args);
230 
231  switch ($function) {
232  case 'AVERAGE':
233  $count = count($args);
234  $array_sum = array_sum($args);
235 
236  return ($count > 0) ? $array_sum / $count : 0;
237  case 'SUM':
238  return array_sum($args);
239  case 'MIN':
240  return min($args);
241  case 'MAX':
242  return max($args);
243  default:
244  throw new ilException("Unrecognized function '$function'");
245  }
246  }
247 
255  protected function calculate(string $operator, $left, $right)
256  {
257  switch ($operator) {
258  case '+':
259  $result = $left + $right;
260  break;
261  case '-':
262  $result = $left - $right;
263  break;
264  case '*':
265  $result = $left * $right;
266  break;
267  case '/':
268  $result = ($right == 0) ? 0 : $left / $right;
269  break;
270  case '^':
271  $result = pow($left, $right);
272  break;
273  default:
274  throw new \ilException("Unrecognized operator '$operator'");
275  }
276 
277  return $result;
278  }
279 
280 }
Token
The string representation of these tokens must not occur in the names of metadata elements...
Definition: Token.php:27
__construct(private FieldSubstitution $substitution, private Tokenizer $tokenizer,)
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
$token
Definition: xapitoken.php:70
getFunctionArgs(int $index, array $data)
Helper method to return the function and its arguments from a preg_replace_all $result array...