ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
MathResultResolver.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
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
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}
__construct(private FieldSubstitution $substitution, private Tokenizer $tokenizer,)
getFunctionArgs(int $index, array $data)
Helper method to return the function and its arguments from a preg_replace_all $result array.
Base class for ILIAS Exception handling.
Token
The string representation of these tokens must not occur in the names of metadata elements.
Definition: Token.php:28
$token
Definition: xapitoken.php:70