PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 4.0.0
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v4.0.0
4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 trunk 1.0.0 2.0.0 2.0.1 2.0.2 2.0.3 3.0 3.0.1 3.0.2 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.2.0 3.2.1 3.2.2 3.2.4 3.2.5 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.4.5 3.4.6 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.6 3.5.7 3.5.8 3.5.9 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1
superb-blocks / src / gutenberg / form / class-form-math-parser.php
superb-blocks / src / gutenberg / form Last commit date
class-form-access-control.php 1 month ago class-form-captcha-handler.php 1 month ago class-form-controller.php 1 month ago class-form-email-config-check.php 1 month ago class-form-email-handler.php 1 month ago class-form-encryption.php 1 month ago class-form-exporter.php 1 month ago class-form-field-validator.php 1 month ago class-form-file-handler.php 1 month ago class-form-google-auth.php 1 month ago class-form-integration-handler.php 1 month ago class-form-math-parser.php 1 month ago class-form-permissions.php 1 month ago class-form-registry.php 1 month ago class-form-settings.php 1 month ago class-form-submission-cpt.php 1 month ago class-form-submission-handler.php 1 month ago
class-form-math-parser.php
308 lines
1 <?php
2
3 namespace SuperbAddons\Gutenberg\Form;
4
5 defined('ABSPATH') || exit();
6
7 /**
8 * Safe math parser for calculated form fields.
9 * Recursive-descent parser — NO eval().
10 *
11 * Supports: +, -, *, /, parentheses, numbers, round(expr, decimals)
12 * Field references: {fieldId} resolved at evaluation time.
13 *
14 * PHP 5.6 compatible: no const class constants, no ??, no short array in const.
15 */
16 class FormMathParser
17 {
18 /**
19 * Evaluate a formula string with field values.
20 *
21 * @param string $formula Formula with {fieldId} references
22 * @param array $field_values Map of fieldId => raw value string
23 * @param int $round_result Decimal places to round final result (-1 = no rounding)
24 * @return float
25 */
26 public static function Evaluate($formula, $field_values, $round_result = -1)
27 {
28 if (!is_string($formula) || $formula === '') {
29 return 0;
30 }
31
32 $tokens = self::Tokenize($formula);
33 if (empty($tokens)) {
34 return 0;
35 }
36
37 $pos = 0;
38 $result = self::ParseExpr($tokens, $pos, $field_values);
39
40 // Handle NaN, Infinity
41 if (!is_finite($result) || is_nan($result)) {
42 return 0;
43 }
44
45 // Apply global rounding
46 if (is_numeric($round_result) && $round_result >= 0) {
47 $result = round($result, intval($round_result));
48 }
49
50 return $result;
51 }
52
53 /**
54 * Extract field IDs referenced in a formula.
55 *
56 * @param string $formula
57 * @return array Array of fieldId strings
58 */
59 public static function ExtractFieldRefs($formula)
60 {
61 if (!is_string($formula) || $formula === '') {
62 return array();
63 }
64
65 $refs = array();
66 if (preg_match_all('/\{([^}]+)\}/', $formula, $matches)) {
67 foreach ($matches[1] as $ref) {
68 if (!in_array($ref, $refs, true)) {
69 $refs[] = $ref;
70 }
71 }
72 }
73
74 return $refs;
75 }
76
77 /**
78 * Clean a field value: strip everything except digits, decimal points, minus signs.
79 *
80 * @param mixed $val
81 * @return string
82 */
83 private static function CleanFieldValue($val)
84 {
85 if ($val === null || $val === '') {
86 return '0';
87 }
88
89 $cleaned = preg_replace('/[^0-9.\-]/', '', strval($val));
90 if ($cleaned === '' || $cleaned === '-' || $cleaned === '.') {
91 return '0';
92 }
93
94 return $cleaned;
95 }
96
97 /**
98 * Tokenize a formula string.
99 *
100 * @param string $formula
101 * @return array Array of token arrays: array('type' => string, 'value' => mixed)
102 */
103 private static function Tokenize($formula)
104 {
105 $tokens = array();
106 $len = strlen($formula);
107 $i = 0;
108
109 while ($i < $len) {
110 $ch = $formula[$i];
111
112 // Whitespace
113 if ($ch === ' ' || $ch === "\t" || $ch === "\n" || $ch === "\r") {
114 $i++;
115 continue;
116 }
117
118 // Field reference: {fieldId}
119 if ($ch === '{') {
120 $end = strpos($formula, '}', $i);
121 if ($end === false) {
122 $i++;
123 continue;
124 }
125 $tokens[] = array('type' => 'FIELD_REF', 'value' => substr($formula, $i + 1, $end - $i - 1));
126 $i = $end + 1;
127 continue;
128 }
129
130 // Number (including negative preceded by operator or start)
131 if (
132 ($ch >= '0' && $ch <= '9') ||
133 $ch === '.' ||
134 ($ch === '-' && (
135 empty($tokens) ||
136 $tokens[count($tokens) - 1]['type'] === 'OP' ||
137 $tokens[count($tokens) - 1]['type'] === 'LPAREN' ||
138 $tokens[count($tokens) - 1]['type'] === 'COMMA'
139 ))
140 ) {
141 $start = $i;
142 if ($ch === '-') {
143 $i++;
144 }
145 while ($i < $len && (($formula[$i] >= '0' && $formula[$i] <= '9') || $formula[$i] === '.')) {
146 $i++;
147 }
148 $num_str = substr($formula, $start, $i - $start);
149 $num = floatval($num_str);
150 $tokens[] = array('type' => 'NUMBER', 'value' => $num);
151 continue;
152 }
153
154 // Function: round
155 if (substr($formula, $i, 5) === 'round' && $i + 5 < $len && $formula[$i + 5] === '(') {
156 $tokens[] = array('type' => 'FUNC', 'value' => 'round');
157 $i += 5;
158 continue;
159 }
160
161 // Operators
162 if ($ch === '+' || $ch === '-' || $ch === '*' || $ch === '/') {
163 $tokens[] = array('type' => 'OP', 'value' => $ch);
164 $i++;
165 continue;
166 }
167
168 // Parentheses
169 if ($ch === '(') {
170 $tokens[] = array('type' => 'LPAREN', 'value' => '(');
171 $i++;
172 continue;
173 }
174 if ($ch === ')') {
175 $tokens[] = array('type' => 'RPAREN', 'value' => ')');
176 $i++;
177 continue;
178 }
179
180 // Comma
181 if ($ch === ',') {
182 $tokens[] = array('type' => 'COMMA', 'value' => ',');
183 $i++;
184 continue;
185 }
186
187 // Unknown — skip
188 $i++;
189 }
190
191 return $tokens;
192 }
193
194 /**
195 * Parse expression: term (('+' | '-') term)*
196 */
197 private static function ParseExpr(&$tokens, &$pos, $field_values)
198 {
199 $left = self::ParseTerm($tokens, $pos, $field_values);
200
201 while (true) {
202 if ($pos >= count($tokens)) {
203 break;
204 }
205 $t = $tokens[$pos];
206 if ($t['type'] !== 'OP' || ($t['value'] !== '+' && $t['value'] !== '-')) {
207 break;
208 }
209 $pos++;
210 $right = self::ParseTerm($tokens, $pos, $field_values);
211 if ($t['value'] === '+') {
212 $left = $left + $right;
213 } else {
214 $left = $left - $right;
215 }
216 }
217
218 return $left;
219 }
220
221 /**
222 * Parse term: factor (('*' | '/') factor)*
223 */
224 private static function ParseTerm(&$tokens, &$pos, $field_values)
225 {
226 $left = self::ParseFactor($tokens, $pos, $field_values);
227
228 while (true) {
229 if ($pos >= count($tokens)) {
230 break;
231 }
232 $t = $tokens[$pos];
233 if ($t['type'] !== 'OP' || ($t['value'] !== '*' && $t['value'] !== '/')) {
234 break;
235 }
236 $pos++;
237 $right = self::ParseFactor($tokens, $pos, $field_values);
238 if ($t['value'] === '*') {
239 $left = $left * $right;
240 } else {
241 // Division by zero returns 0
242 $left = $right == 0 ? 0 : $left / $right;
243 }
244 }
245
246 return $left;
247 }
248
249 /**
250 * Parse factor: NUMBER | FIELD_REF | '(' expr ')' | round '(' expr ',' expr ')'
251 */
252 private static function ParseFactor(&$tokens, &$pos, $field_values)
253 {
254 if ($pos >= count($tokens)) {
255 return 0;
256 }
257
258 $t = $tokens[$pos];
259
260 // Number literal
261 if ($t['type'] === 'NUMBER') {
262 $pos++;
263 return $t['value'];
264 }
265
266 // Field reference
267 if ($t['type'] === 'FIELD_REF') {
268 $pos++;
269 $raw = isset($field_values[$t['value']]) ? $field_values[$t['value']] : '';
270 $cleaned = self::CleanFieldValue($raw);
271 $num = floatval($cleaned);
272 return is_nan($num) ? 0 : $num;
273 }
274
275 // Parenthesized expression
276 if ($t['type'] === 'LPAREN') {
277 $pos++; // (
278 $val = self::ParseExpr($tokens, $pos, $field_values);
279 if ($pos < count($tokens) && $tokens[$pos]['type'] === 'RPAREN') {
280 $pos++; // )
281 }
282 return $val;
283 }
284
285 // round(expr, decimals)
286 if ($t['type'] === 'FUNC' && $t['value'] === 'round') {
287 $pos++; // round
288 if ($pos < count($tokens) && $tokens[$pos]['type'] === 'LPAREN') {
289 $pos++; // (
290 }
291 $expr = self::ParseExpr($tokens, $pos, $field_values);
292 $decimals = 0;
293 if ($pos < count($tokens) && $tokens[$pos]['type'] === 'COMMA') {
294 $pos++; // ,
295 $decimals = self::ParseExpr($tokens, $pos, $field_values);
296 }
297 if ($pos < count($tokens) && $tokens[$pos]['type'] === 'RPAREN') {
298 $pos++; // )
299 }
300 return round($expr, max(0, intval($decimals)));
301 }
302
303 // Unknown token — skip and return 0
304 $pos++;
305 return 0;
306 }
307 }
308