Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
61 / 61
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
Evaluator
100.00% covered (success)
100.00%
61 / 61
100.00% covered (success)
100.00%
3 / 3
27
100.00% covered (success)
100.00%
1 / 1
 evaluate
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
11
 parseValue
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 shuntingYard
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
13
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Math\Parser
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\Math\Parser;
16
17/**
18 * Basic math function evaluation.
19 *
20 * @package phpOMS\Math\Parser
21 * @license OMS License 2.0
22 * @link    https://jingga.app
23 * @since   1.0.0
24 */
25final class Evaluator
26{
27    /**
28     * Evaluate function.
29     *
30     * @param string $equation Formula to evaluate
31     *
32     * @return null|float
33     *
34     * @since 1.0.0
35     */
36    public static function evaluate(string $equation) : ?float
37    {
38        if (\substr_count($equation, '(') !== \substr_count($equation, ')')
39            || \preg_match('#[^0-9\+\-\*\/\(\)\ \^\.]#', $equation)
40        ) {
41            return null;
42        }
43
44        $stack   = [];
45        $postfix = self::shuntingYard($equation);
46
47        foreach ($postfix as $value) {
48            if (\is_numeric($value)) {
49                $stack[] = $value;
50            } else {
51                $a = self::parseValue(\array_pop($stack) ?? 0);
52                $b = self::parseValue(\array_pop($stack) ?? 0);
53
54                if ($value === '+') {
55                    $stack[] = $a + $b;
56                } elseif ($value === '-') {
57                    $stack[] = $b - $a;
58                } elseif ($value === '*') {
59                    $stack[] = $a * $b;
60                } elseif ($value === '/') {
61                    $stack[] = $b / $a;
62                } elseif ($value === '^') {
63                    $stack[] = $b ** $a;
64                }
65            }
66        }
67
68        $result = \array_pop($stack);
69
70        return \is_numeric($result) ? (float) $result : null;
71    }
72
73    /**
74     * Parse value.
75     *
76     * @param int|float|string $value Value to parse
77     *
78     * @return int|float
79     *
80     * @since 1.0.0
81     */
82    private static function parseValue(int | float | string $value) : int | float
83    {
84        return \is_string($value)
85            ? (\stripos($value, '.') === false ? (int) $value : (float) $value)
86            : $value;
87    }
88
89    /**
90     * Shunting Yard algorithm.
91     *
92     * @param string $equation Equation to convert
93     *
94     * @return string[]
95     *
96     * @since 1.0.0
97     */
98    private static function shuntingYard(string $equation) : array
99    {
100        $stack     = [];
101        $operators = [
102            '^' => ['precedence' => 4, 'order' => 1],
103            '*' => ['precedence' => 3, 'order' => -1],
104            '/' => ['precedence' => 3, 'order' => -1],
105            '+' => ['precedence' => 2, 'order' => -1],
106            '-' => ['precedence' => 2, 'order' => -1],
107        ];
108        $output    = [];
109
110        $equation = \str_replace(' ', '', $equation);
111        $equation = \preg_split('/([\+\-\*\/\^\(\)])/', $equation, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);
112
113        if ($equation === false) {
114            return []; // @codeCoverageIgnore
115        }
116
117        $equation = \array_filter($equation, function($n) {
118            return $n !== '';
119        });
120
121        foreach ($equation as $i => $token) {
122            if (\is_numeric($token)) {
123                $output[] = $token;
124            } elseif (\strpbrk($token, '^*/+-') !== false) {
125                $o1 = $token;
126                $o2 = \end($stack);
127
128                while ($o2 !== false && \strpbrk($o2, '^*/+-') !== false
129                    && (($operators[$o1]['order'] === -1 && $operators[$o1]['precedence'] <= $operators[$o2]['precedence'])
130                        /*|| ($operators[$o1]['order'] === 1 && $operators[$o1]['precedence'] < $operators[$o2]['precedence'])*/)
131                ) {
132                    // The commented part above is always FALSE because this equation always compares 4 < 2|3|4.
133                    // Only uncomment if the opperators array changes.
134                    $output[] = \array_pop($stack);
135                    $o2       = \end($stack);
136                }
137
138                $stack[] = $o1;
139            } elseif ($token === '(') {
140                $stack[] = $token;
141            } elseif ($token === ')') {
142                while (\end($stack) !== '(') {
143                    $output[] = \array_pop($stack);
144                }
145
146                \array_pop($stack);
147            }
148        }
149
150        while (!empty($stack)) {
151            $output[] = \array_pop($stack);
152        }
153
154        /** @var string[] $output */
155        return $output;
156    }
157}