1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Calc.php |
5
|
|
|
* |
6
|
|
|
* @date 28.03.2015 0:54:24 |
7
|
|
|
* @copyright Sklyarov Alexey <[email protected]> |
8
|
|
|
*/ |
9
|
|
|
|
10
|
|
|
namespace Sufir\Calc; |
11
|
|
|
|
12
|
|
|
use Sufir\Calc\Token; |
13
|
|
|
use SplStack; |
14
|
|
|
use Closure; |
15
|
|
|
use ReflectionObject; |
16
|
|
|
use RuntimeException; |
17
|
|
|
use Sufir\Calc\Token\NumberToken; |
18
|
|
|
use Sufir\Calc\Token\FunctionToken; |
19
|
|
|
use Sufir\Calc\Token\VariableToken; |
20
|
|
|
use InvalidArgumentException; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Calc |
24
|
|
|
* |
25
|
|
|
* Description of Calc |
26
|
|
|
* |
27
|
|
|
* @author Sklyarov Alexey <[email protected]> |
28
|
|
|
* @package Sufir\Calc |
29
|
|
|
*/ |
30
|
|
|
final class Calc |
31
|
|
|
{ |
32
|
|
|
private $functions = []; |
33
|
|
|
|
34
|
|
|
private $variables = []; |
35
|
|
|
|
36
|
|
|
//protected $allowedPhpFunctions = false; |
|
|
|
|
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @param integer $scale |
40
|
|
|
*/ |
41
|
17 |
|
public function __construct($scale = 5) |
42
|
|
|
{ |
43
|
17 |
|
bcscale($scale); |
44
|
17 |
|
} |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* |
48
|
|
|
* @param Token $tokens |
49
|
|
|
* @param array $variables |
50
|
|
|
* @return string |
51
|
|
|
* @throws RuntimeException |
52
|
|
|
*/ |
53
|
14 |
|
public function evaluate(array $tokens, array $variables = []) |
|
|
|
|
54
|
|
|
{ |
55
|
14 |
|
$stack = new SplStack; |
56
|
|
|
|
57
|
|
|
//var_dump($tokens); |
58
|
|
|
|
59
|
14 |
|
foreach ($tokens as $token) { |
60
|
14 |
|
if ($token->isNumber()) { |
61
|
14 |
|
$stack->push($token); |
62
|
|
|
|
63
|
14 |
|
} elseif ($token->isVariable()) { |
64
|
4 |
|
if (!isset($this->variables[$token->getValue()])) { |
65
|
1 |
|
throw new RuntimeException( |
66
|
1 |
|
"Undefined variable: «{$token}»" |
67
|
1 |
|
); |
68
|
|
|
} |
69
|
|
|
|
70
|
3 |
|
$stack->push(new NumberToken($this->variables[$token->getValue()])); |
71
|
|
|
|
72
|
14 |
|
} elseif ($token->isOperator()) { |
73
|
12 |
|
if ($stack->count() > 1) { |
74
|
12 |
|
$second = $stack->pop(); |
75
|
12 |
|
$first = $stack->pop(); |
76
|
|
|
|
77
|
12 |
|
$result = self::math($token->getValue(), $first->getValue(), $second->getValue()); |
78
|
|
|
|
79
|
12 |
|
$stack->push(new NumberToken($result)); |
80
|
12 |
|
} else { |
81
|
1 |
|
throw new RuntimeException( |
82
|
1 |
|
"Not enough operands for a binary operator: «{$token}»" |
83
|
1 |
|
); |
84
|
|
|
} |
85
|
|
|
|
86
|
14 |
|
} elseif ($token->isFunction()) { |
87
|
9 |
|
if (!isset($this->functions[$token->getValue()])) { |
88
|
1 |
|
throw new RuntimeException("Undefined function: «{$token}()»"); |
89
|
|
|
} |
90
|
|
|
|
91
|
8 |
|
$countOfArguments = $this->functions[$token->getValue()]['args']; |
92
|
|
|
|
93
|
8 |
|
if ($stack->count() < $countOfArguments) { |
94
|
1 |
|
throw new RuntimeException( |
95
|
1 |
|
"Wrong argenents for function «{$token}()»:" |
96
|
1 |
|
. "defined {$stack->count()} expected {$countOfArguments}" |
97
|
1 |
|
); |
98
|
|
|
} |
99
|
|
|
|
100
|
7 |
|
$arguments = []; |
101
|
7 |
|
for ($idx = 0; $idx < $countOfArguments; $idx++) { |
102
|
4 |
|
$arguments[] = $stack->pop()->getValue(); |
103
|
4 |
|
} |
104
|
7 |
|
$arguments = array_reverse($arguments); |
105
|
|
|
|
106
|
7 |
|
$result = call_user_func_array($this->functions[$token->getValue()]['func'], $arguments); |
107
|
|
|
|
108
|
7 |
|
if (!is_numeric($result)) { |
109
|
1 |
|
throw new RuntimeException( |
110
|
1 |
|
"Wrong result type of «{$token->getValue()}()» function, expected integer or float" |
111
|
1 |
|
); |
112
|
|
|
} |
113
|
|
|
|
114
|
6 |
|
$stack->push(new NumberToken($result)); |
115
|
6 |
|
} |
116
|
14 |
|
} |
117
|
|
|
|
118
|
9 |
|
return $stack->top()->getValue(); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Register new function. |
123
|
|
|
* |
124
|
|
|
* @param string $name |
125
|
|
|
* @param Closure $callable |
126
|
|
|
* @return \Sufir\Calc\Calc |
127
|
|
|
*/ |
128
|
9 |
|
public function registerFunction($name, Closure $callable) |
129
|
|
|
{ |
130
|
9 |
|
$name = rtrim($name, " \t\n\r\0\x0B\(\)"); |
131
|
9 |
|
if (!FunctionToken::validate($name)) { |
132
|
1 |
|
throw new InvalidArgumentException("Wrong function name «{$name}»"); |
133
|
|
|
} |
134
|
|
|
|
135
|
8 |
|
$objReflector = new ReflectionObject($callable); |
136
|
8 |
|
$reflector = $objReflector->getMethod('__invoke'); |
137
|
8 |
|
$parameters = $reflector->getParameters(); |
138
|
|
|
|
139
|
8 |
|
$this->functions[$name] = array( |
140
|
8 |
|
'args' => count($parameters), |
141
|
8 |
|
'func' => $callable, |
142
|
|
|
); |
143
|
|
|
|
144
|
8 |
|
return $this; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Define new variable. |
149
|
|
|
* |
150
|
|
|
* @param string $name |
151
|
|
|
* @param integer $value |
152
|
|
|
* @return \Sufir\Calc\Calc |
153
|
|
|
* @throws RuntimeException |
154
|
|
|
*/ |
155
|
5 |
|
public function defineVar($name, $value) |
156
|
|
|
{ |
157
|
5 |
|
$name = '$' . ltrim($name, " \t\n\r\0\x0B\$"); |
158
|
5 |
|
if (!VariableToken::validate($name)) { |
159
|
1 |
|
throw new InvalidArgumentException("Wrong variable name «{$name}»"); |
160
|
|
|
} |
161
|
|
|
|
162
|
4 |
|
if (!is_numeric($value)) { |
163
|
1 |
|
throw new InvalidArgumentException( |
164
|
1 |
|
"Wrong type of «{$name}», expected integer or float" |
165
|
1 |
|
); |
166
|
|
|
} |
167
|
|
|
|
168
|
3 |
|
$this->variables[$name] = $value; |
169
|
|
|
|
170
|
3 |
|
return $this; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* @param string $operator |
175
|
|
|
* @param integer|float $firstOperand |
176
|
|
|
* @param integer|float $secondOperand |
177
|
|
|
* @return int |
178
|
|
|
*/ |
179
|
12 |
|
private static function math($operator, $firstOperand, $secondOperand) |
180
|
|
|
{ |
181
|
|
|
switch ($operator) { |
182
|
12 |
|
case '^': |
183
|
4 |
|
return bcpow($firstOperand, $secondOperand); |
184
|
12 |
|
case '*': |
185
|
9 |
|
return bcmul($firstOperand, $secondOperand); |
186
|
5 |
|
case '/': |
187
|
3 |
|
return bcdiv($firstOperand, $secondOperand); |
188
|
5 |
|
case '+': |
189
|
5 |
|
return bcadd($firstOperand, $secondOperand); |
190
|
4 |
|
case '-': |
191
|
4 |
|
return bcsub($firstOperand, $secondOperand); |
192
|
|
|
} |
193
|
|
|
} |
194
|
|
|
} |
195
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.