PluginProbe
Cooked – Recipe Management / 1.16.1
Cooked – Recipe Management v1.16.1
1.16.1 1.16.0 1.15.0 trunk 1.10.0 1.11.0 1.11.1 1.11.2 1.11.3 1.11.4 1.12.0 1.13.0 1.14.0 1.7.10 1.7.11 1.7.12 1.7.13 1.7.15.1 1.7.15.3 1.7.15.4 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 All 37 releases
cooked / vendor / nxp / math-executor / tests / MathTest.php

MathTest.php in Cooked – Recipe Management 1.16.1, at vendor/nxp/math-executor/tests/MathTest.php

1,164 lines 38.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * This file is part of the MathExecutor package
5 *
6 * (c) Alexander Kiryukhin
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code
10 */
11
12 namespace NXP\Tests;
13
14 use Exception;
15 use NXP\Exception\DivisionByZeroException;
16 use NXP\Exception\IncorrectExpressionException;
17 use NXP\Exception\IncorrectNumberOfFunctionParametersException;
18 use NXP\Exception\MathExecutorException;
19 use NXP\Exception\UnknownFunctionException;
20 use NXP\Exception\UnknownVariableException;
21 use NXP\MathExecutor;
22 use PHPUnit\Framework\TestCase;
23
24 class MathTest extends TestCase
25 {
26 /**
27 * @dataProvider providerExpressions
28 */
29 public function testCalculating(string $expression) : void
30 {
31 $calculator = new MathExecutor();
32
33 /** @var float $phpResult */
34 eval('$phpResult = ' . $expression . ';');
35
36 try {
37 $result = $calculator->execute($expression);
38 } catch (Exception $e) {
39 $this->fail(\sprintf('Exception: %s (%s:%d), expression was: %s', \get_class($e), $e->getFile(), $e->getLine(), $expression));
40 }
41 $this->assertEquals($phpResult, $result, "Expression was: {$expression}");
42 }
43
44 /**
45 * Expressions data provider
46 *
47 * Most tests can go in here. The idea is that each expression will be evaluated by MathExecutor and by PHP with eval.
48 * The results should be the same. If they are not, then the test fails. No need to add extra test unless you are doing
49 * something more complex and not a simple mathmatical expression.
50 */
51 public function providerExpressions()
52 {
53 return [
54 ['-5'],
55 ['-5+10'],
56 ['4-5'],
57 ['4 -5'],
58 ['(4*2)-5'],
59 ['(4*2) - 5'],
60 ['4*-5'],
61 ['4 * -5'],
62 ['+5'],
63 ['+(3+2)'],
64 ['+(+3+2)'],
65 ['+(-3+2)'],
66 ['-5'],
67 ['-(-5)'],
68 ['-(+5)'],
69 ['+(-5)'],
70 ['+(+5)'],
71 ['-(3+2)'],
72 ['-(-3+-2)'],
73
74 ['abs(1.5)'],
75 ['acos(0.15)'],
76 ['acosh(1.5)'],
77 ['asin(0.15)'],
78 ['atan(0.15)'],
79 ['atan2(1.5, 3.5)'],
80 ['atanh(0.15)'],
81 ['bindec("10101")'],
82 ['ceil(1.5)'],
83 ['cos(1.5)'],
84 ['cosh(1.5)'],
85 ['decbin("15")'],
86 ['dechex("15")'],
87 ['decoct("15")'],
88 ['deg2rad(1.5)'],
89 ['exp(1.5)'],
90 ['expm1(1.5)'],
91 ['floor(1.5)'],
92 ['fmod(1.5, 3.5)'],
93 ['hexdec("abcdef")'],
94 ['hypot(1.5, 3.5)'],
95 ['intdiv(10, 2)'],
96 ['log(1.5)'],
97 ['log10(1.5)'],
98 ['log1p(1.5)'],
99 ['max(1.5, 3.5)'],
100 ['min(1.5, 3.5)'],
101 ['octdec("15")'],
102 ['pi()'],
103 ['pow(1.5, 3.5)'],
104 ['rad2deg(1.5)'],
105 ['round(1.5)'],
106 ['sin(1.5)'],
107 ['sin(12)'],
108 ['+sin(12)'],
109 ['-sin(12)'],
110 ['sinh(1.5)'],
111 ['sqrt(1.5)'],
112 ['tan(1.5)'],
113 ['tanh(1.5)'],
114
115 ['0.1 + 0.2'],
116 ['0.1 + 0.2 - 0.3'],
117 ['1 + 2'],
118
119 ['0.1 - 0.2'],
120 ['1 - 2'],
121
122 ['0.1 * 2'],
123 ['1 * 2'],
124
125 ['0.1 / 0.2'],
126 ['1 / 2'],
127
128 ['2 * 2 + 3 * 3'],
129 ['2 * 2 / 3 * 3'],
130 ['2 / 2 / 3 / 3'],
131 ['2 / 2 * 3 / 3'],
132 ['2 / 2 * 3 * 3'],
133
134 ['1 + 0.6 - 3 * 2 / 50'],
135
136 ['(5 + 3) * -1'],
137
138 ['-2- 2*2'],
139 ['2- 2*2'],
140 ['2-(2*2)'],
141 ['(2- 2)*2'],
142 ['2 + 2*2'],
143 ['2+ 2*2'],
144 ['2+2*2'],
145 ['(2+2)*2'],
146 ['(2 + 2)*-2'],
147 ['(2+-2)*2'],
148
149 ['1 + 2 * 3 / (min(1, 5) + 2 + 1)'],
150 ['1 + 2 * 3 / (min(1, 5) - 2 + 5)'],
151 ['1 + 2 * 3 / (min(1, 5) * 2 + 1)'],
152 ['1 + 2 * 3 / (min(1, 5) / 2 + 1)'],
153 ['1 + 2 * 3 / (min(1, 5) / 2 * 1)'],
154 ['1 + 2 * 3 / (min(1, 5) / 2 / 1)'],
155 ['1 + 2 * 3 / (3 + min(1, 5) + 2 + 1)'],
156 ['1 + 2 * 3 / (3 - min(1, 5) - 2 + 1)'],
157 ['1 + 2 * 3 / (3 * min(1, 5) * 2 + 1)'],
158 ['1 + 2 * 3 / (3 / min(1, 5) / 2 + 1)'],
159
160 ['(1 + 2) * 3 / (3 / min(1, 5) / 2 + 1)'],
161
162 ['sin(10) * cos(50) / min(10, 20/2)'],
163 ['sin(10) * cos(50) / min(10, (20/2))'],
164 ['sin(10) * cos(50) / min(10, (max(10,20)/2))'],
165
166 ['100500 * 3.5e5'],
167 ['100500 * 3.5e-5'],
168 ['100500 * 3.5E5'],
169 ['100500 * 3.5E-5'],
170
171 ['1 + "2" / 3'],
172 ["1.5 + '2.5' / 4"],
173 ['1.5 + "2.5" * ".5"'],
174
175 ['-1 + -2'],
176 ['-1+-2'],
177 ['-1- -2'],
178 ['-1/-2'],
179 ['-1*-2'],
180
181 ['(1+2+3+4-5)*7/100'],
182 ['(-1+2+3+4- 5)*7/100'],
183 ['(1+2+3+4- 5)*7/100'],
184 ['( 1 + 2 + 3 + 4 - 5 ) * 7 / 100'],
185
186 ['1 && 0'],
187 ['1 && 0 && 1'],
188 ['1 || 0'],
189 ['1 && 0 || 1'],
190
191 ['5 == 3'],
192 ['5 == 5'],
193 ['5 != 3'],
194 ['5 != 5'],
195 ['5 > 3'],
196 ['3 > 5'],
197 ['3 >= 5'],
198 ['3 >= 3'],
199 ['3 < 5'],
200 ['5 < 3'],
201 ['3 <= 5'],
202 ['5 <= 5'],
203 ['10 < 9 || 4 > (2+1)'],
204 ['10 < 9 || 4 > (-2+1)'],
205 ['10 < 9 || 4 > (2+1) && 5 == 5 || 4 != 6 || 3 >= 4 || 3 <= 7'],
206
207 ['1 + 5 == 3 + 1'],
208 ['1 + 5 == 5 + 1'],
209 ['1 + 5 != 3 + 1'],
210 ['1 + 5 != 5 + 1'],
211 ['1 + 5 > 3 + 1'],
212 ['1 + 3 > 5 + 1'],
213 ['1 + 3 >= 5 + 1'],
214 ['1 + 3 >= 3 + 1'],
215 ['1 + 3 < 5 + 1'],
216 ['1 + 5 < 3 + 1'],
217 ['1 + 3 <= 5 + 1'],
218 ['1 + 5 <= 5 + 1'],
219
220 ['(-4)'],
221 ['(-4 + 5)'],
222 ['(3 * 1)'],
223 ['(-3 * -1)'],
224 ['1 + (-3 * -1)'],
225 ['1 + ( -3 * 1)'],
226 ['1 + (3 *-1)'],
227 ['1 - 0'],
228 ['1-0'],
229
230 ['-(1.5)'],
231 ['-log(4)'],
232 ['0-acosh(1.5)'],
233 ['-acosh(1.5)'],
234 ['-(-4)'],
235 ['-(-4 + 5)'],
236 ['-(3 * 1)'],
237 ['-(-3 * -1)'],
238 ['-1 + (-3 * -1)'],
239 ['-1 + ( -3 * 1)'],
240 ['-1 + (3 *-1)'],
241 ['-1 - 0'],
242 ['-1-0'],
243 ['-(4*2)-5'],
244 ['-(4*-2)-5'],
245 ['-(-4*2) - 5'],
246 ['-4*-5'],
247 ['max(1,2,4.9,3)'],
248 ['min(1,2,4.9,3)'],
249 ['max([1,2,4.9,3])'],
250 ['min([1,2,4.9,3])'],
251
252 ['4 % 4'],
253 ['7 % 4'],
254 ['99 % 4'],
255 ['123 % 7'],
256 ];
257 }
258
259 /**
260 * @dataProvider bcMathExpressions
261 */
262 public function testBCMathCalculating(string $expression, string $expected = '') : void
263 {
264 $calculator = new MathExecutor();
265 $calculator->useBCMath();
266
267 if ('' === $expected)
268 {
269 $expected = $expression;
270 }
271
272 /** @var float $phpResult */
273 eval('$phpResult = ' . $expected . ';');
274
275 try {
276 $result = $calculator->execute($expression);
277 } catch (Exception $e) {
278 $this->fail(\sprintf('Exception: %s (%s:%d), expression was: %s', \get_class($e), $e->getFile(), $e->getLine(), $expression));
279 }
280 $this->assertEquals($phpResult, $result, "Expression was: {$expression}");
281 }
282
283 /**
284 * Expressions data provider
285 *
286 * Most tests can go in here. The idea is that each expression will be evaluated by MathExecutor and by PHP with eval.
287 * The results should be the same. If they are not, then the test fails. No need to add extra test unless you are doing
288 * something more complex and not a simple mathmatical expression.
289 */
290 public function bcMathExpressions()
291 {
292 return [
293 ['-5'],
294 ['-5+10'],
295 ['4-5'],
296 ['4 -5'],
297 ['(4*2)-5'],
298 ['(4*2) - 5'],
299 ['4*-5'],
300 ['4 * -5'],
301 ['+5'],
302 ['+(3+2)'],
303 ['+(+3+2)'],
304 ['+(-3+2)'],
305 ['-5'],
306 ['-(-5)'],
307 ['-(+5)'],
308 ['+(-5)'],
309 ['+(+5)'],
310 ['-(3+2)'],
311 ['-(-3+-2)'],
312
313 ['abs(1.5)'],
314 ['acos(0.15)'],
315 ['acosh(1.5)'],
316 ['asin(0.15)'],
317 ['atan(0.15)'],
318 ['atan2(1.5, 3.5)'],
319 ['atanh(0.15)'],
320 ['bindec("10101")'],
321 ['ceil(1.5)'],
322 ['cos(1.5)'],
323 ['cosh(1.5)'],
324 ['decbin("15")'],
325 ['dechex("15")'],
326 ['decoct("15")'],
327 ['deg2rad(1.5)'],
328 ['exp(1.5)'],
329 ['expm1(1.5)'],
330 ['floor(1.5)'],
331 ['fmod(1.5, 3.5)'],
332 ['hexdec("abcdef")'],
333 ['hypot(1.5, 3.5)'],
334 ['intdiv(10, 2)'],
335 ['log(1.5)'],
336 ['log10(1.5)'],
337 ['log1p(1.5)'],
338 ['max(1.5, 3.5)'],
339 ['min(1.5, 3.5)'],
340 ['octdec("15")'],
341 ['pi()'],
342 ['pow(1.5, 3.5)'],
343 ['rad2deg(1.5)'],
344 ['round(1.5)'],
345 ['sin(1.5)'],
346 ['sin(12)'],
347 ['+sin(12)'],
348 ['-sin(12)', '0.53'],
349 ['sinh(1.5)'],
350 ['sqrt(1.5)'],
351 ['tan(1.5)'],
352 ['tanh(1.5)'],
353
354 ['0.1 + 0.2', '0.30'],
355 ['0.1 + 0.2 - 0.3', '0.00'],
356 ['1 + 2'],
357
358 ['0.1 - 0.2'],
359 ['1 - 2'],
360
361 ['0.1 * 2'],
362 ['1 * 2'],
363
364 ['0.1 / 0.2'],
365 ['1 / 2'],
366
367 ['2 * 2 + 3 * 3'],
368 ['2 * 2 / 3 * 3', '3.99'],
369 ['2 / 2 / 3 / 3', '0.11'],
370 ['2 / 2 * 3 / 3'],
371 ['2 / 2 * 3 * 3'],
372
373 ['1 + 0.6 - 3 * 2 / 50'],
374
375 ['(5 + 3) * -1'],
376
377 ['-2- 2*2'],
378 ['2- 2*2'],
379 ['2-(2*2)'],
380 ['(2- 2)*2'],
381 ['2 + 2*2'],
382 ['2+ 2*2'],
383 ['2+2*2'],
384 ['(2+2)*2'],
385 ['(2 + 2)*-2'],
386 ['(2+-2)*2'],
387
388 ['1 + 2 * 3 / (min(1, 5) + 2 + 1)'],
389 ['1 + 2 * 3 / (min(1, 5) - 2 + 5)'],
390 ['1 + 2 * 3 / (min(1, 5) * 2 + 1)'],
391 ['1 + 2 * 3 / (min(1, 5) / 2 + 1)'],
392 ['1 + 2 * 3 / (min(1, 5) / 2 * 1)'],
393 ['1 + 2 * 3 / (min(1, 5) / 2 / 1)'],
394 ['1 + 2 * 3 / (3 + min(1, 5) + 2 + 1)', '1.85'],
395 ['1 + 2 * 3 / (3 - min(1, 5) - 2 + 1)'],
396 ['1 + 2 * 3 / (3 * min(1, 5) * 2 + 1)', '1.85'],
397 ['1 + 2 * 3 / (3 / min(1, 5) / 2 + 1)'],
398
399 ['(1 + 2) * 3 / (3 / min(1, 5) / 2 + 1)'],
400
401 ['sin(10) * cos(50) / min(10, 20/2)', '-0.05'],
402 ['sin(10) * cos(50) / min(10, (20/2))', '-0.05'],
403 ['sin(10) * cos(50) / min(10, (max(10,20)/2))', '-0.05'],
404
405 ['1 + "2" / 3', '1.66'],
406 ["1.5 + '2.5' / 4", '2.12'],
407 ['1.5 + "2.5" * ".5"'],
408
409 ['-1 + -2'],
410 ['-1+-2'],
411 ['-1- -2'],
412 ['-1/-2'],
413 ['-1*-2'],
414
415 ['(1+2+3+4-5)*7/100'],
416 ['(-1+2+3+4- 5)*7/100'],
417 ['(1+2+3+4- 5)*7/100'],
418 ['( 1 + 2 + 3 + 4 - 5 ) * 7 / 100'],
419
420 ['1 && 0'],
421 ['1 && 0 && 1'],
422 ['1 || 0'],
423 ['1 && 0 || 1'],
424
425 ['5 == 3'],
426 ['5 == 5'],
427 ['5 != 3'],
428 ['5 != 5'],
429 ['5 > 3'],
430 ['3 > 5'],
431 ['3 >= 5'],
432 ['3 >= 3'],
433 ['3 < 5'],
434 ['5 < 3'],
435 ['3 <= 5'],
436 ['5 <= 5'],
437 ['10 < 9 || 4 > (2+1)'],
438 ['10 < 9 || 4 > (-2+1)'],
439 ['10 < 9 || 4 > (2+1) && 5 == 5 || 4 != 6 || 3 >= 4 || 3 <= 7'],
440
441 ['1 + 5 == 3 + 1'],
442 ['1 + 5 == 5 + 1'],
443 ['1 + 5 != 3 + 1'],
444 ['1 + 5 != 5 + 1'],
445 ['1 + 5 > 3 + 1'],
446 ['1 + 3 > 5 + 1'],
447 ['1 + 3 >= 5 + 1'],
448 ['1 + 3 >= 3 + 1'],
449 ['1 + 3 < 5 + 1'],
450 ['1 + 5 < 3 + 1'],
451 ['1 + 3 <= 5 + 1'],
452 ['1 + 5 <= 5 + 1'],
453
454 ['(-4)'],
455 ['(-4 + 5)'],
456 ['(3 * 1)'],
457 ['(-3 * -1)'],
458 ['1 + (-3 * -1)'],
459 ['1 + ( -3 * 1)'],
460 ['1 + (3 *-1)'],
461 ['1 - 0'],
462 ['1-0'],
463
464 ['-(1.5)'],
465 ['-log(4)', '-1.38'],
466 ['0-acosh(1.5)', '-0.96'],
467 ['-acosh(1.5)', '-0.96'],
468 ['-(-4)'],
469 ['-(-4 + 5)'],
470 ['-(3 * 1)'],
471 ['-(-3 * -1)'],
472 ['-1 + (-3 * -1)'],
473 ['-1 + ( -3 * 1)'],
474 ['-1 + (3 *-1)'],
475 ['-1 - 0'],
476 ['-1-0'],
477 ['-(4*2)-5'],
478 ['-(4*-2)-5'],
479 ['-(-4*2) - 5'],
480 ['-4*-5'],
481 ['max(1,2,4.9,3)'],
482 ['min(1,2,4.9,3)'],
483 ['max([1,2,4.9,3])'],
484 ['min([1,2,4.9,3])'],
485
486 ['4 % 4'],
487 ['7 % 4'],
488 ['99 % 4'],
489 ['123 % 7'],
490 ];
491 }
492
493 /**
494 * @dataProvider incorrectExpressions
495 */
496 public function testIncorrectExpressionException(string $expression) : void
497 {
498 $calculator = new MathExecutor();
499 $calculator->setVars(['a' => 12, 'b' => 24]);
500 $this->expectException(IncorrectExpressionException::class);
501 $calculator->execute($expression);
502 }
503
504 /**
505 * Incorrect Expressions data provider
506 *
507 * These expressions should not pass validation
508 */
509 public function incorrectExpressions()
510 {
511 return [
512 ['1 * + '],
513 [' 2 3'],
514 ['2 3 '],
515 [' 2 4 3 '],
516 ['$a $b'],
517 ['$a [3, 4, 5]'],
518 ['$a (3 + 4)'],
519 ['$a "string"'],
520 ['5 "string"'],
521 ['"string" $a'],
522 ['$a round(12.345)'],
523 ['round(12.345) $a'],
524 ['4 round(12.345)'],
525 ['round(12.345) 4'],
526 ];
527 }
528
529 public function testUnknownFunctionException() : void
530 {
531 $calculator = new MathExecutor();
532 $this->expectException(UnknownFunctionException::class);
533 $calculator->execute('1 * fred("wilma") + 3');
534 }
535
536 public function testZeroDivision() : void
537 {
538 $calculator = new MathExecutor();
539 $calculator->setDivisionByZeroIsZero();
540 $this->assertEquals(0, $calculator->execute('10 / 0'));
541 }
542
543 public function testUnaryOperators() : void
544 {
545 $calculator = new MathExecutor();
546 $this->assertEquals(5, $calculator->execute('+5'));
547 $this->assertEquals(5, $calculator->execute('+(3+2)'));
548 $this->assertEquals(-5, $calculator->execute('-5'));
549 $this->assertEquals(5, $calculator->execute('-(-5)'));
550 $this->assertEquals(-5, $calculator->execute('+(-5)'));
551 $this->assertEquals(-5, $calculator->execute('-(3+2)'));
552 }
553
554 public function testZeroDivisionException() : void
555 {
556 $calculator = new MathExecutor();
557 $this->expectException(DivisionByZeroException::class);
558 $calculator->execute('10 / 0');
559 $calculator->setVar('one', 1)->setVar('zero', 0);
560 $this->assertEquals(0.0, $calculator->execute('$one / $zero'));
561 }
562
563 public function testVariableIncorrectExpressionException() : void
564 {
565 $calculator = new MathExecutor();
566 $calculator->setVar('four', 4);
567 $this->assertEquals(4, $calculator->execute('$four'));
568 $this->expectException(IncorrectExpressionException::class);
569 $this->assertEquals(0.0, $calculator->execute('$'));
570 $this->assertEquals(0.0, $calculator->execute('$ + $four'));
571 }
572
573 public function testExponentiation() : void
574 {
575 $calculator = new MathExecutor();
576 $this->assertEquals(100, $calculator->execute('10 ^ 2'));
577 }
578
579 public function testStringEscape() : void
580 {
581 $calculator = new MathExecutor();
582 $this->assertEquals("test\string", $calculator->execute('"test\string"'));
583 $this->assertEquals("\\test\string\\", $calculator->execute('"\test\string\\\\"'));
584 $this->assertEquals('\test\string\\', $calculator->execute('"\test\string\\\\"'));
585 $this->assertEquals('test\\\\string', $calculator->execute('"test\\\\\\\\string"'));
586 $this->assertEquals('test"string', $calculator->execute('"test\"string"'));
587 $this->assertEquals('test""string', $calculator->execute('"test\"\"string"'));
588 $this->assertEquals('"teststring', $calculator->execute('"\"teststring"'));
589 $this->assertEquals('teststring"', $calculator->execute('"teststring\""'));
590 $this->assertEquals("test'string", $calculator->execute("'test\'string'"));
591 $this->assertEquals("test''string", $calculator->execute("'test\'\'string'"));
592 $this->assertEquals("'teststring", $calculator->execute("'\'teststring'"));
593 $this->assertEquals("teststring'", $calculator->execute("'teststring\''"));
594
595 $calculator->addFunction('concat', static function($arg1, $arg2) {
596 return $arg1 . $arg2;
597 });
598 $this->assertEquals('test"ing', $calculator->execute('concat("test\"","ing")'));
599 $this->assertEquals("test'ing", $calculator->execute("concat('test\'','ing')"));
600 }
601
602 public function testArrays() : void
603 {
604 $calculator = new MathExecutor();
605 $this->assertEquals([1, 5, 2], $calculator->execute('array(1, 5, 2)'));
606 $this->assertEquals([1, 5, 2], $calculator->execute('[1, 5, 2]'));
607 $this->assertEquals(\max([1, 5, 2]), $calculator->execute('max([1, 5, 2])'));
608 $this->assertEquals(\max([1, 5, 2]), $calculator->execute('max(array(1, 5, 2))'));
609 $calculator->addFunction('arr_with_max_elements', static function($arg1, ...$args) {
610 $args = \is_array($arg1) ? $arg1 : [$arg1, ...$args];
611 \usort($args, static fn ($arr1, $arr2) => \count($arr2) <=> \count($arr1));
612
613 return $args[0];
614 });
615 $this->assertEquals([3, 3, 3], $calculator->execute('arr_with_max_elements([[1],array(2,2),[3,3,3]])'));
616 }
617
618 public function testFunctionParameterOrder() : void
619 {
620 $calculator = new MathExecutor();
621
622 $calculator->addFunction('concat', static function($arg1, $arg2) {
623 return $arg1 . $arg2;
624 });
625 $this->assertEquals('testing', $calculator->execute('concat("test","ing")'));
626 $this->assertEquals('testing', $calculator->execute("concat('test','ing')"));
627 }
628
629 public function testFunction() : void
630 {
631 $calculator = new MathExecutor();
632 $calculator->addFunction('round', static function($arg) {
633 return \round($arg);
634 });
635 $this->assertEquals(\round(100 / 30), $calculator->execute('round(100/30)'));
636 }
637
638 public function testFunctionUnlimitedParameters() : void
639 {
640 $calculator = new MathExecutor();
641 $calculator->addFunction('give_me_an_array', static function() {
642 return [5, 3, 7, 9, 8];
643 });
644 $this->assertEquals(6.4, $calculator->execute('avg(give_me_an_array())'));
645 $this->assertEquals(10, $calculator->execute('avg(12,8,15,5)'));
646 $this->assertEquals(3, $calculator->execute('min(give_me_an_array())'));
647 $this->assertEquals(1, $calculator->execute('min(1,2,3)'));
648 $this->assertEquals(9, $calculator->execute('max(give_me_an_array())'));
649 $this->assertEquals(3, $calculator->execute('max(1,2,3)'));
650 $calculator->setVar('monthly_salaries', [100, 200, 300]);
651 $this->assertEquals([100, 200, 300], $calculator->execute('$monthly_salaries'));
652 $this->assertEquals(200, $calculator->execute('avg($monthly_salaries)'));
653 $this->assertEquals(\min([100, 200, 300]), $calculator->execute('min($monthly_salaries)'));
654 $this->assertEquals(\max([100, 200, 300]), $calculator->execute('max($monthly_salaries)'));
655 }
656
657 public function testFunctionOptionalParameters() : void
658 {
659 $calculator = new MathExecutor();
660 $calculator->addFunction('round', static function($num, $precision = 0) {
661 return \round($num, $precision);
662 });
663 $this->assertEquals(\round(11.176), $calculator->execute('round(11.176)'));
664 $this->assertEquals(\round(11.176, 2), $calculator->execute('round(11.176,2)'));
665 }
666
667 public function testFunctionIncorrectNumberOfParameters() : void
668 {
669 $calculator = new MathExecutor();
670 $this->expectException(IncorrectNumberOfFunctionParametersException::class);
671 $calculator->addFunction('myfunc', static function($arg1, $arg2) {
672 return $arg1 + $arg2;
673 });
674 $calculator->execute('myfunc(1)');
675 }
676
677 public function testFunctionIncorrectNumberOfParametersTooMany() : void
678 {
679 $calculator = new MathExecutor();
680 $this->expectException(IncorrectNumberOfFunctionParametersException::class);
681 $calculator->addFunction('myfunc', static function($arg1, $arg2) {
682 return $arg1 + $arg2;
683 });
684 $calculator->execute('myfunc(1,2,3)');
685 }
686
687 public function testFunctionIf() : void
688 {
689 $calculator = new MathExecutor();
690 $this->assertEquals(
691 30,
692 $calculator->execute(
693 'if(100 > 99, 30, 0)'
694 ),
695 'Expression failed: if(100 > 99, 30, 0)'
696 );
697 $this->assertEquals(
698 0,
699 $calculator->execute(
700 'if(100 < 99, 30, 0)'
701 ),
702 'Expression failed: if(100 < 99, 30, 0)'
703 );
704 $this->assertEquals(
705 30,
706 $calculator->execute(
707 'if(98 < 99 && sin(1) < 1, 30, 0)'
708 ),
709 'Expression failed: if(98 < 99 && sin(1) < 1, 30, 0)'
710 );
711 $this->assertEquals(
712 40,
713 $calculator->execute(
714 'if(98 < 99 && sin(1) < 1, max(30, 40), 0)'
715 ),
716 'Expression failed: if(98 < 99 && sin(1) < 1, max(30, 40), 0)'
717 );
718 $this->assertEquals(
719 40,
720 $calculator->execute(
721 'if(98 < 99 && sin(1) < 1, if(10 > 5, max(30, 40), 1), 0)'
722 ),
723 'Expression failed: if(98 < 99 && sin(1) < 1, if(10 > 5, max(30, 40), 1), 0)'
724 );
725 $this->assertEquals(
726 20,
727 $calculator->execute(
728 'if(98 < 99 && sin(1) > 1, if(10 > 5, max(30, 40), 1), if(4 <= 4, 20, 21))'
729 ),
730 'Expression failed: if(98 < 99 && sin(1) > 1, if(10 > 5, max(30, 40), 1), if(4 <= 4, 20, 21))'
731 );
732 $this->assertEquals(
733 \cos(2),
734 $calculator->execute(
735 'if(98 < 99 && sin(1) >= 1, max(30, 40), cos(2))'
736 ),
737 'Expression failed: if(98 < 99 && sin(1) >= 1, max(30, 40), cos(2))'
738 );
739 $this->assertEquals(
740 \cos(2),
741 $calculator->execute(
742 'if(cos(2), cos(2), 0)'
743 ),
744 'Expression failed: if(cos(2), cos(2), 0)'
745 );
746 $trx_amount = 100000;
747 $calculator->setVar('trx_amount', $trx_amount);
748 $this->assertEquals($trx_amount, $calculator->execute('$trx_amount'));
749 $this->assertEquals(
750 $trx_amount * 0.03,
751 $calculator->execute(
752 'if($trx_amount < 40000, $trx_amount * 0.06, $trx_amount * 0.03)'
753 ),
754 'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06, $trx_amount * 0.03)'
755 );
756 $this->assertEquals(
757 $trx_amount * 0.03,
758 $calculator->execute(
759 'if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))'
760 ),
761 'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))'
762 );
763 $trx_amount = 39000;
764 $calculator->setVar('trx_amount', $trx_amount);
765 $this->assertEquals(
766 $trx_amount * 0.06,
767 $calculator->execute(
768 'if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))'
769 ),
770 'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))'
771 );
772 $trx_amount = 59000;
773 $calculator->setVar('trx_amount', $trx_amount);
774 $this->assertEquals(
775 $trx_amount * 0.05,
776 $calculator->execute(
777 'if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))'
778 ),
779 'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))'
780 );
781 $this->expectException(IncorrectNumberOfFunctionParametersException::class);
782 $this->assertEquals(
783 0.0,
784 $calculator->execute(
785 'if($trx_amount < 40000, $trx_amount * 0.06)'
786 ),
787 'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06)'
788 );
789 }
790
791 public function testVariables() : void
792 {
793 $calculator = new MathExecutor();
794 $this->assertEquals(3.14159265359, $calculator->execute('$pi'));
795 $this->assertEquals(3.14159265359, $calculator->execute('pi'));
796 $this->assertEquals(2.71828182846, $calculator->execute('$e'));
797 $this->assertEquals(2.71828182846, $calculator->execute('e'));
798 $calculator->setVars([
799 'trx_amount' => 100000.01,
800 'ten' => 10,
801 'nine' => 9,
802 'eight' => 8,
803 'seven' => 7,
804 'six' => 6,
805 'five' => 5,
806 'four' => 4,
807 'three' => 3,
808 'two' => 2,
809 'one' => 1,
810 'zero' => 0,
811 ]);
812 $this->assertEquals(100000.01, $calculator->execute('$trx_amount'));
813 $this->assertEquals(10 - 9, $calculator->execute('$ten - $nine'));
814 $this->assertEquals(9 - 10, $calculator->execute('$nine - $ten'));
815 $this->assertEquals(10 + 9, $calculator->execute('$ten + $nine'));
816 $this->assertEquals(10 * 9, $calculator->execute('$ten * $nine'));
817 $this->assertEquals(10 / 9, $calculator->execute('$ten / $nine'));
818 $this->assertEquals(10 / (9 / 5), $calculator->execute('$ten / ($nine / $five)'));
819
820 // test variables without leading $
821 $this->assertEquals(100000.01, $calculator->execute('trx_amount'));
822 $this->assertEquals(10 - 9, $calculator->execute('ten - nine'));
823 $this->assertEquals(9 - 10, $calculator->execute('nine - ten'));
824 $this->assertEquals(10 + 9, $calculator->execute('ten + nine'));
825 $this->assertEquals(10 * 9, $calculator->execute('ten * nine'));
826 $this->assertEquals(10 / 9, $calculator->execute('ten / nine'));
827 $this->assertEquals(10 / (9 / 5), $calculator->execute('ten / (nine / five)'));
828 }
829
830 public function testEvaluateFunctionParameters() : void
831 {
832 $calculator = new MathExecutor();
833 $calculator->addFunction(
834 'round',
835 static function($value, $decimals) {
836 return \round($value, $decimals);
837 }
838 );
839 $expression = 'round(100 * 1.111111, 2)';
840 $phpResult = 0;
841 eval('$phpResult = ' . $expression . ';');
842 $this->assertEquals($phpResult, $calculator->execute($expression));
843 $expression = 'round((100*0.04)+(((100*1.02)+0.5)*1.28),2)';
844 eval('$phpResult = ' . $expression . ';');
845 $this->assertEquals($phpResult, $calculator->execute($expression));
846 }
847
848 public function testFunctionsWithQuotes() : void
849 {
850 $calculator = new MathExecutor();
851 $calculator->addFunction('concat', static function($first, $second) {
852 return $first . $second;
853 });
854 $this->assertEquals('testing', $calculator->execute('concat("test", "ing")'));
855 $this->assertEquals('testing', $calculator->execute("concat('test', 'ing')"));
856 }
857
858 public function testQuotes() : void
859 {
860 $calculator = new MathExecutor();
861 $testString = 'some, long. arg; with: different-separators!';
862 $calculator->addFunction(
863 'test',
864 function($arg) use ($testString) {
865 $this->assertEquals($testString, $arg);
866
867 return 0;
868 }
869 );
870 $calculator->execute('test("' . $testString . '")'); // single quotes
871 $calculator->execute("test('" . $testString . "')"); // double quotes
872 }
873
874 public function testBeginWithBracketAndMinus() : void
875 {
876 $calculator = new MathExecutor();
877 $this->assertEquals(-4, $calculator->execute('(-4)'));
878 $this->assertEquals(1, $calculator->execute('(-4 + 5)'));
879 }
880
881 public function testStringComparison() : void
882 {
883 $calculator = new MathExecutor();
884 $this->assertEquals(true, $calculator->execute('"a" == \'a\''));
885 $this->assertEquals(true, $calculator->execute('"hello world" == "hello world"'));
886 $this->assertEquals(false, $calculator->execute('"hello world" == "hola mundo"'));
887 $this->assertEquals(true, $calculator->execute('"hello world" != "hola mundo"'));
888 $this->assertEquals(true, $calculator->execute('"a" < "b"'));
889 $this->assertEquals(false, $calculator->execute('"a" > "b"'));
890 $this->assertEquals(true, $calculator->execute('"a" <= "b"'));
891 $this->assertEquals(false, $calculator->execute('"a" >= "b"'));
892 $this->assertEquals(true, $calculator->execute('"A" != "a"'));
893 }
894
895 public function testVarStringComparison() : void
896 {
897 $calculator = new MathExecutor();
898 $calculator->setVar('var', 97);
899 $this->assertEquals(false, $calculator->execute('97 == "a"'));
900 $this->assertEquals(false, $calculator->execute('$var == "a"'));
901 $calculator->setVar('var', 'a');
902 $this->assertEquals(true, $calculator->execute('$var == "a"'));
903 }
904
905 public function testOnVarNotFound() : void
906 {
907 $calculator = new MathExecutor();
908 $calculator->setVarNotFoundHandler(
909 static function($varName) {
910 if ('undefined' == $varName) {
911 return 3;
912 }
913 }
914 );
915 $this->assertEquals(15, $calculator->execute('5 * undefined'));
916 $this->assertEquals(3, $calculator->getVar('undefined'));
917 $this->assertNull($calculator->getVar('Lucy'));
918 }
919
920 public function testGetVarException() : void
921 {
922 $calculator = new MathExecutor();
923 $this->expectException(UnknownVariableException::class);
924 $this->assertNull($calculator->getVar('Lucy'));
925 }
926
927 public function testMinusZero() : void
928 {
929 $calculator = new MathExecutor();
930 $this->assertEquals(1, $calculator->execute('1 - 0'));
931 $this->assertEquals(1, $calculator->execute('1-0'));
932 }
933
934 public function testScientificNotation() : void
935 {
936 $calculator = new MathExecutor();
937 $this->assertEquals(1.5e9, $calculator->execute('1.5e9'));
938 $this->assertEquals(1.5e-9, $calculator->execute('1.5e-9'));
939 $this->assertEquals(1.5e+9, $calculator->execute('1.5e+9'));
940 }
941
942 public function testNullReturnType() : void
943 {
944 $calculator = new MathExecutor();
945 $calculator->setVar('nullValue', null);
946 $this->assertEquals(null, $calculator->execute('nullValue'));
947 }
948
949 public function testGetFunctionsReturnsArray() : void
950 {
951 $calculator = new MathExecutor();
952 $this->assertIsArray($calculator->getFunctions());
953 }
954
955 public function testGetFunctionsReturnsFunctions() : void
956 {
957 $calculator = new MathExecutor();
958 $this->assertGreaterThan(40, \count($calculator->getFunctions()));
959 }
960
961 public function testGetVarsReturnsArray() : void
962 {
963 $calculator = new MathExecutor();
964 $this->assertIsArray($calculator->getVars());
965 }
966
967 public function testGetVarsReturnsCount() : void
968 {
969 $calculator = new MathExecutor();
970 $this->assertGreaterThan(1, \count($calculator->getVars()));
971 }
972
973 public function testUndefinedVarThrowsExecption() : void
974 {
975 $calculator = new MathExecutor();
976 $this->assertGreaterThan(1, \count($calculator->getVars()));
977 $this->expectException(UnknownVariableException::class);
978 $calculator->execute('5 * undefined');
979 }
980
981 public function testSetVarsAcceptsAllScalars() : void
982 {
983 $calculator = new MathExecutor();
984 $calculator->setVar('boolTrue', true);
985 $calculator->setVar('boolFalse', false);
986 $calculator->setVar('int', 1);
987 $calculator->setVar('null', null);
988 $calculator->setVar('float', 1.1);
989 $calculator->setVar('string', 'string');
990 $this->assertCount(8, $calculator->getVars());
991 $this->assertEquals(true, $calculator->getVar('boolTrue'));
992 $this->assertEquals(false, $calculator->getVar('boolFalse'));
993 $this->assertEquals(1, $calculator->getVar('int'));
994 $this->assertEquals(null, $calculator->getVar('null'));
995 $this->assertEquals(1.1, $calculator->getVar('float'));
996 $this->assertEquals('string', $calculator->getVar('string'));
997
998 $this->expectException(MathExecutorException::class);
999 $calculator->setVar('validVar', new \DateTime());
1000 }
1001
1002 public function testSetVarsDoesNotAcceptObject() : void
1003 {
1004 $calculator = new MathExecutor();
1005 $this->expectException(MathExecutorException::class);
1006 $calculator->setVar('object', $this);
1007 }
1008
1009 public function testSetVarsDoesNotAcceptResource() : void
1010 {
1011 $calculator = new MathExecutor();
1012 $this->expectException(MathExecutorException::class);
1013 $calculator->setVar('resource', \tmpfile());
1014 }
1015
1016 public function testSetCustomVarValidator() : void
1017 {
1018 $calculator = new MathExecutor();
1019 $calculator->setVarValidationHandler(static function(string $name, $variable) : void {
1020 // allow all scalars and null
1021 if (\is_scalar($variable) || null === $variable) {
1022 return;
1023 }
1024 // Allow variables of type DateTime, but not others
1025 if (! $variable instanceof \DateTime) {
1026 throw new MathExecutorException('Invalid variable type');
1027 }
1028 });
1029
1030 $calculator->setVar('validFloat', 0.0);
1031 $calculator->setVar('validInt', 0);
1032 $calculator->setVar('validTrue', true);
1033 $calculator->setVar('validFalse', false);
1034 $calculator->setVar('validString', 'string');
1035 $calculator->setVar('validNull', null);
1036 $calculator->setVar('validDateTime', new \DateTime());
1037
1038 $this->expectException(MathExecutorException::class);
1039 $calculator->setVar('validVar', $this);
1040 }
1041
1042 public function testSetCustomVarNameValidator() : void
1043 {
1044 $calculator = new MathExecutor();
1045 $calculator->setVarValidationHandler(static function(string $name, $variable) : void {
1046 // don't allow variable names with the word invalid in them
1047 if (\str_contains($name, 'invalid')) {
1048 throw new MathExecutorException('Invalid variable name');
1049 }
1050 });
1051
1052 $calculator->setVar('validFloat', 0.0);
1053 $calculator->setVar('validInt', 0);
1054 $calculator->setVar('validTrue', true);
1055 $calculator->setVar('validFalse', false);
1056 $calculator->setVar('validString', 'string');
1057 $calculator->setVar('validNull', null);
1058 $calculator->setVar('validDateTime', new \DateTime());
1059
1060 $this->expectException(MathExecutorException::class);
1061 $calculator->setVar('invalidVar', 12);
1062 }
1063
1064 public function testVarExists() : void
1065 {
1066 $calculator = new MathExecutor();
1067 $varName = 'Eythel';
1068 $calculator->setVar($varName, 1);
1069 $this->assertTrue($calculator->varExists($varName));
1070 $this->assertFalse($calculator->varExists('Lucy'));
1071 }
1072
1073 /**
1074 * @dataProvider providerExpressionValues
1075 */
1076 public function testCalculatingValues($expression, $value) : void
1077 {
1078 $calculator = new MathExecutor();
1079
1080 try {
1081 $result = $calculator->execute($expression);
1082 } catch (Exception $e) {
1083 $this->fail(\sprintf('Exception: %s (%s:%d), expression was: %s', \get_class($e), $e->getFile(), $e->getLine(), $expression));
1084 }
1085 $this->assertEquals($value, $result, "{$expression} did not evaluate to {$value}");
1086 }
1087
1088 /**
1089 * Expressions data provider
1090 *
1091 * Most tests can go in here. The idea is that each expression will be evaluated by MathExecutor and by PHP directly.
1092 * The results should be the same. If they are not, then the test fails. No need to add extra test unless you are doing
1093 * something more complex and not a simple mathmatical expression.
1094 */
1095 public function providerExpressionValues()
1096 {
1097 return [
1098 ['arccos(0.5)', \acos(0.5)],
1099 ['arccosec(4)', \asin(1 / 4)],
1100 ['arccot(3)', M_PI / 2 - \atan(3)],
1101 ['arccotan(4)', M_PI / 2 - \atan(4)],
1102 ['arccsc(4)', \asin(1 / 4)],
1103 ['arcctg(3)', M_PI / 2 - \atan(3)],
1104 ['arcsec(4)', \acos(1 / 4)],
1105 ['arcsin(0.5)', \asin(0.5)],
1106 ['arctan(0.5)', \atan(0.5)],
1107 ['arctan(4)', \atan(4)],
1108 ['arctg(0.5)', \atan(0.5)],
1109 ['cosec(12)', 1 / \sin(12)],
1110 ['cosec(4)', 1 / \sin(4)],
1111 ['cosh(12)', \cosh(12)],
1112 ['cot(12)', \cos(12) / \sin(12)],
1113 ['cotan(12)', \cos(12) / \sin(12)],
1114 ['cotan(4)', \cos(4) / \sin(4)],
1115 ['cotg(3)', \cos(3) / \sin(3)],
1116 ['csc(4)', 1 / \sin(4)],
1117 ['ctg(4)', \cos(4) / \sin(4)],
1118 ['ctn(4)', \cos(4) / \sin(4)],
1119 ['decbin(10)', \decbin(10)],
1120 ['lg(2)', \log10(2)],
1121 ['ln(2)', \log(2)],
1122 ['sec(4)', 1 / \cos(4)],
1123 ['tg(4)', \tan(4)],
1124 ];
1125 }
1126
1127 public function testCache() : void
1128 {
1129 $calculator = new MathExecutor();
1130 $this->assertEquals(256, $calculator->execute('2 ^ 8')); // second arg $cache is true by default
1131
1132 $this->assertIsArray($calculator->getCache());
1133 $this->assertCount(1, $calculator->getCache());
1134
1135 $this->assertEquals(512, $calculator->execute('2 ^ 9', true));
1136 $this->assertCount(2, $calculator->getCache());
1137
1138 $this->assertEquals(1024, $calculator->execute('2 ^ 10', false));
1139 $this->assertCount(2, $calculator->getCache());
1140
1141 $calculator->clearCache();
1142 $this->assertIsArray($calculator->getCache());
1143 $this->assertCount(0, $calculator->getCache());
1144
1145 $this->assertEquals(2048, $calculator->execute('2 ^ 11', false));
1146 $this->assertCount(0, $calculator->getCache());
1147 }
1148
1149 public function testUnsupportedOperands() : void
1150 {
1151 if (\version_compare(PHP_VERSION, '8') >= 0) {
1152 $calculator = new MathExecutor();
1153
1154 $calculator->setVar('stringVar', 'string');
1155 $calculator->setVar('intVar', 1);
1156
1157 $this->expectException(\TypeError::class);
1158 $calculator->execute('stringVar + intVar');
1159 } else {
1160 $this->expectNotToPerformAssertions();
1161 }
1162 }
1163 }
1164