Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
Parser.php
1<?php
2
3namespace Bitrix\Bizproc\Calc;
4
5class Parser
6{
7 private const Operation = 0;
8 private const Variable = 1;
9 private const Constant = 2;
10
11 private \CBPActivity $activity;
12 private array $errors = [];
13 private array $priority = [
14 '(' => 0,
15 ')' => 1,
16 '@' => 2,
17 ';' => 2,
18 '=' => 3,
19 '<' => 3,
20 '>' => 3,
21 '<=' => 3,
22 '>=' => 3,
23 '<>' => 3,
24 '&' => 4,
25 '+' => 5,
26 '-' => 5,
27 '*' => 6,
28 '/' => 6,
29 '^' => 7,
30 '%' => 8,
31 '-m' => 9,
32 '+m' => 9,
33 ' ' => 10,
34 ':' => 11,
35 'f' => 12,
36 ];
37
38 // Allowable functions
39 private array $functions;
40
41 private array $errorMessages = [
42 0 => 'Incorrect variable name - "#STR#"',
43 1 => 'Empty',
44 2 => 'Syntax error "#STR#"',
45 3 => 'Unknown function "#STR#"',
46 4 => 'Unmatched closing bracket ")"',
47 5 => 'Unmatched opening bracket "("',
48 6 => 'Division by zero',
49 7 => 'Incorrect order of operands',
50 8 => 'Incorrect arguments of function "#STR#"',
51 ];
52
53 public function __construct(\CBPActivity $activity)
54 {
55 $this->activity = $activity;
56 $this->functions = Functions::getList();
57 }
58
59 public function getActivity(): \CBPActivity
60 {
61 return $this->activity;
62 }
63
64 private function getVariableValue($variable)
65 {
66 $variable = trim($variable);
67 if (!preg_match(\CBPActivity::ValuePattern, $variable))
68 {
69 return null;
70 }
71
72 return $this->activity->parseValue($variable);
73 }
74
75 private function setError($errorCode, $message = ''): void
76 {
77 $this->errors[] = [$errorCode, str_replace('#STR#', $message, $this->errorMessages[$errorCode])];
78 }
79
80 public function getErrors(): array
81 {
82 return $this->errors;
83 }
84
85 /*
86 Return array of polish notation
87 array(
88 key => array(value, self::Operation | self::Variable | self::Constant)
89 )
90 */
91 private function getPolishNotation($text)
92 {
93 $text = trim($text);
94 if (mb_strpos($text, '=') === 0)
95 {
96 $text = mb_substr($text, 1);
97 }
98 if (mb_strpos($text, '{{=') === 0 && mb_substr($text, -2) === '}}')
99 {
100 $text = mb_substr($text, 3);
101 $text = mb_substr($text, 0, -2);
102 }
103
104 if (!$text)
105 {
106 $this->setError(1);
107
108 return false;
109 }
110
111 $notation = [];
112 $stack = [];
113 $prev = '';
114
115 $isFunctionArgs = function ($stack)
116 {
117 return (
118 isset($stack[0], $stack[1], $this->functions[$stack[1][0]]) && $stack[0][0] === '('
119 );
120 };
121
122 $preg = '/
123 \s*\‍(\s* |
124 \s*\‍)\s* |
125 \s*,\s* | # Combine ranges of variables
126 \s*;\s* | # Combine ranges of variables
127 \s*=\s* |
128 \s*<=\s* |
129 \s*>=\s* |
130 \s*<>\s* |
131 \s*<\s* |
132 \s*>\s* |
133 \s*&\s* | # String concatenation
134 \s*\+\s* | # Addition or unary plus
135 \s*-\s* |
136 \s*\*\s* |
137 \s*\/\s* |
138 \s*\^\s* | # Exponentiation
139 \s*%\s* | # Percent
140 \s*[\d.]+\s* | # Numbers
141 \s*\'[^\']*\'\s* | # String constants in apostrophes
142 \s*"[^"]*"\s* | # String constants in quotes
143 (\s*\w+\s*\‍(\s*) | # Function names
144 \s*' . \CBPActivity::ValueInternalPattern . '\s* | # Variables
145 (?<error>.+) # Any erroneous substring
146 /xi';
147
148 while (preg_match($preg, $text, $match))
149 {
150 if (isset($match['error']))
151 {
152 $this->setError(2, $match['error']);
153
154 return false;
155 }
156
157 $str = trim($match[0]);
158 if ($str === ",")
159 {
160 $str = ";";
161 }
162
163 if (isset($match[1]) && $match[1])
164 {
165 $str = mb_strtolower($str);
166 [$name] = explode('(', $str);
167 $name = trim($name);
168 if (isset($this->functions[$name]))
169 {
170 if ($stack)
171 {
172 while ($this->priority['f'] <= $stack[0][1])
173 {
174 $op = array_shift($stack);
175 $notation[] = [$op[0], self::Operation];
176 if (!$stack)
177 {
178 break;
179 }
180 }
181 }
182 array_unshift($stack, [$name, $this->priority['f']]);
183 }
184 else
185 {
186 $this->setError(3, $name);
187
188 return false;
189 }
190 $str = '(';
191 }
192
193 if ($str === '-' || $str === '+')
194 {
195 if (
196 $prev === ''
197 || in_array($prev, ['(', ';', '=', '<=', '>=', '<>', '<', '>', '&', '+', '-', '*', '/', '^'])
198 )
199 {
200 $str .= 'm';
201 }
202 }
203
204 switch ($str)
205 {
206 case '(':
207 array_unshift($stack, ['(', $this->priority['(']]);
208 array_unshift($stack, ['@', $this->priority['@']]);
209 break;
210 case ')':
211 $hasComma = false;
212 $hasArguments = $prev !== '(';
213 //trailing comma
214 if ($prev === ';')
215 {
216 array_shift($stack);
217 }
218
219 while ($op = array_shift($stack))
220 {
221 if ($op[0] === '(')
222 {
223 break;
224 }
225 if ($op[0] === ';')
226 {
227 $hasComma = true;
228 }
229
230 $isInFunction = $isFunctionArgs($stack);
231
232 if (
233 $op[0] === '@'
234 && (
235 (!$hasComma && !$isInFunction)
236 || (!$hasArguments && $isInFunction)
237 )
238 )
239 {
240 continue;
241 }
242 $notation[] = [$op[0], self::Operation];
243 }
244 if ($op === null)
245 {
246 $this->setError(4);
247
248 return false;
249 }
250 break;
251 case ';' :
252 case '=' :
253 case '<=':
254 case '>=':
255 case '<>':
256 case '<' :
257 case '>' :
258 case '&' :
259 case '+' :
260 case '-' :
261 case '+m':
262 case '-m':
263 case '*' :
264 case '/' :
265 case '^' :
266 case '%' :
267 if (!$stack)
268 {
269 array_unshift($stack, [$str, $this->priority[$str]]);
270 if ($str === ';')
271 {
272 $notation[] = ['@', self::Operation];
273 }
274 break;
275 }
276 while ($this->priority[$str] <= $stack[0][1])
277 {
278 $op = array_shift($stack);
279 $notation[] = [$op[0], self::Operation];
280 if (!$stack)
281 {
282 break;
283 }
284 }
285 array_unshift($stack, [$str, $this->priority[$str]]);
286 break;
287 default:
288 if (mb_strpos($str, '0') === 0 || (int)$str)
289 {
290 $notation[] = [(float)$str, self::Constant];
291 break;
292 }
293 if (mb_strpos($str, '"') === 0 || mb_strpos($str, "'") === 0)
294 {
295 $notation[] = [mb_substr($str, 1, -1), self::Constant];
296 break;
297 }
298 $notation[] = [$str, self::Variable];
299 }
300 $text = mb_substr($text, mb_strlen($match[0]));
301 $prev = $str;
302 }
303 while ($op = array_shift($stack))
304 {
305 if ($op[0] === '(')
306 {
307 $this->setError(5);
308
309 return false;
310 }
311 $notation[] = [$op[0], self::Operation];
312 }
313
314 return $notation;
315 }
316
317 public function calculate($text)
318 {
319 if (!$notation = $this->getPolishNotation($text))
320 {
321 return null;
322 }
323
324 $stack = [];
325 foreach ($notation as $item)
326 {
327 switch ($item[1])
328 {
329 case self::Constant:
330 array_unshift($stack, $item[0]);
331 break;
332 case self::Variable:
333 array_unshift($stack, $this->getVariableValue($item[0]));
334 break;
335 case self::Operation:
336 switch ($item[0])
337 {
338 case '@':
339 $arg = array_shift($stack);
340 array_unshift($stack, [$arg]);
341 break;
342 case ';':
343 $arg2 = array_shift($stack);
344 $arg1 = array_shift($stack);
345 if (!is_array($arg1) || !isset($arg1[0]))
346 {
347 $arg1 = [$arg1];
348 }
349 $arg1[] = $arg2;
350 array_unshift($stack, $arg1);
351 break;
352 case '=':
353 $arg2 = array_shift($stack);
354 $arg1 = array_shift($stack);
355 array_unshift($stack, $arg1 == $arg2);
356 break;
357 case '<=':
358 $arg2 = array_shift($stack);
359 $arg1 = array_shift($stack);
360 array_unshift($stack, $arg1 <= $arg2);
361 break;
362 case '>=':
363 $arg2 = array_shift($stack);
364 $arg1 = array_shift($stack);
365 array_unshift($stack, $arg1 >= $arg2);
366 break;
367 case '<>':
368 $arg2 = array_shift($stack);
369 $arg1 = array_shift($stack);
370 array_unshift($stack, $arg1 != $arg2);
371 break;
372 case '<':
373 $arg2 = array_shift($stack);
374 $arg1 = array_shift($stack);
375 array_unshift($stack, $arg1 < $arg2);
376 break;
377 case '>':
378 $arg2 = array_shift($stack);
379 $arg1 = array_shift($stack);
380 array_unshift($stack, $arg1 > $arg2);
381 break;
382 case '&':
383 $arg2 = \CBPHelper::stringify(array_shift($stack));
384 $arg1 = \CBPHelper::stringify(array_shift($stack));
385 array_unshift($stack, $arg1 . $arg2);
386 break;
387 case '+':
388 $arg2 = (float)($this->toSingleValue(array_shift($stack)));
389 $arg1 = (float)($this->toSingleValue(array_shift($stack)));
390 array_unshift($stack, $arg1 + $arg2);
391 break;
392 case '-':
393 $arg2 = (float)($this->toSingleValue(array_shift($stack)));
394 $arg1 = (float)($this->toSingleValue(array_shift($stack)));
395 array_unshift($stack, $arg1 - $arg2);
396 break;
397 case '+m':
398 $arg = (float)array_shift($stack);
399 array_unshift($stack, $arg);
400 break;
401 case '-m':
402 $arg = (float)array_shift($stack);
403 array_unshift($stack, (-$arg));
404 break;
405 case '*':
406 $arg2 = (float)($this->toSingleValue(array_shift($stack)));
407 $arg1 = (float)($this->toSingleValue(array_shift($stack)));
408 array_unshift($stack, $arg1 * $arg2);
409 break;
410 case '/':
411 $arg2 = (float)($this->toSingleValue(array_shift($stack)));
412 $arg1 = (float)($this->toSingleValue(array_shift($stack)));
413 if (0 == $arg2)
414 {
415 $this->setError(6);
416
417 return null;
418 }
419 array_unshift($stack, $arg1 / $arg2);
420 break;
421 case '^':
422 $arg2 = (float)array_shift($stack);
423 $arg1 = (float)array_shift($stack);
424 array_unshift($stack, $arg1 ** $arg2);
425 break;
426 case '%':
427 $arg = (float)array_shift($stack);
428 array_unshift($stack, $arg / 100);
429 break;
430 default:
431 $func = $this->functions[$item[0]]['func'];
432 $functionArgs = new Arguments($this);
433 if (!empty($this->functions[$item[0]]['args']))
434 {
435 $args = array_shift($stack);
436 $functionArgs->setArgs(is_array($args) ? $args : [$args]);
437 }
438
439 $val = $func($functionArgs);
440
441 $error = is_float($val) && (is_nan($val) || is_infinite($val));
442 if ($error)
443 {
444 $this->setError(8, $item[0]);
445
446 return null;
447 }
448 array_unshift($stack, $val);
449 }
450 }
451 }
452 if (count($stack) > 1)
453 {
454 $this->setError(7);
455
456 return null;
457 }
458
459 return array_shift($stack);
460 }
461
462 private function toSingleValue($argument)
463 {
464 if (is_array($argument))
465 {
466 reset($argument);
467 return current($argument);
468 }
469
470 return $argument;
471 }
472}
__construct(\CBPActivity $activity)
Definition Parser.php:53