*/ protected array $tokens = []; protected int $pos = 0; /** @var array */ protected array $variables = []; protected bool $guardDivZero = false; protected bool $divByZeroHit = false; /** * @param array $variables */ public function evaluate(string $expression, array $variables = [], bool $guardDivZero = false): ?float { $this->variables = array_map(fn ($v) => $v === null ? 0.0 : (float) $v, $variables); $this->guardDivZero = $guardDivZero; $this->divByZeroHit = false; $this->tokens = $this->tokenize($expression); $this->pos = 0; if (empty($this->tokens)) { return null; } $result = $this->parseExpression(); if ($this->pos < count($this->tokens)) { throw new RuntimeException('Ungkapan formula tidak sah berhampiran: '.($this->tokens[$this->pos]['value'] ?? '')); } if ($this->guardDivZero && $this->divByZeroHit) { return 0.0; } return $result; } /** * @return array */ protected function tokenize(string $expr): array { $tokens = []; $len = strlen($expr); $i = 0; while ($i < $len) { $ch = $expr[$i]; if (ctype_space($ch)) { $i++; continue; } if (in_array($ch, ['+', '-', '*', '/', '(', ')'], true)) { $tokens[] = ['type' => 'op', 'value' => $ch]; $i++; continue; } // Nombor (termasuk perpuluhan) if (ctype_digit($ch) || ($ch === '.' && $i + 1 < $len && ctype_digit($expr[$i + 1]))) { $num = ''; while ($i < $len && (ctype_digit($expr[$i]) || $expr[$i] === '.')) { $num .= $expr[$i]; $i++; } $tokens[] = ['type' => 'num', 'value' => $num]; continue; } // Token / pemboleh ubah: huruf, nombor, underscore (cth: SKOP1, A, D4I) if (ctype_alpha($ch) || $ch === '_') { $tok = ''; while ($i < $len && (ctype_alnum($expr[$i]) || $expr[$i] === '_')) { $tok .= $expr[$i]; $i++; } $tokens[] = ['type' => 'var', 'value' => $tok]; continue; } throw new RuntimeException("Aksara tidak sah dalam formula: '$ch'"); } return $tokens; } protected function peek(): ?array { return $this->tokens[$this->pos] ?? null; } protected function consume(): ?array { return $this->tokens[$this->pos++] ?? null; } /** expression = term (('+' | '-') term)* */ protected function parseExpression(): float { $value = $this->parseTerm(); while (($t = $this->peek()) && $t['type'] === 'op' && in_array($t['value'], ['+', '-'], true)) { $op = $this->consume()['value']; $right = $this->parseTerm(); $value = $op === '+' ? $value + $right : $value - $right; } return $value; } /** term = factor (('*' | '/') factor)* */ protected function parseTerm(): float { $value = $this->parseFactor(); while (($t = $this->peek()) && $t['type'] === 'op' && in_array($t['value'], ['*', '/'], true)) { $op = $this->consume()['value']; $right = $this->parseFactor(); if ($op === '*') { $value *= $right; } else { if ($right == 0.0) { $this->divByZeroHit = true; if ($this->guardDivZero) { $value = 0.0; continue; } throw new RuntimeException('Pembahagian dengan sifar dalam formula.'); } $value /= $right; } } return $value; } /** factor = number | variable | '(' expression ')' | ('+'|'-') factor */ protected function parseFactor(): float { $t = $this->peek(); if ($t === null) { throw new RuntimeException('Ungkapan formula tidak lengkap.'); } if ($t['type'] === 'op' && $t['value'] === '(') { $this->consume(); $value = $this->parseExpression(); $close = $this->consume(); if (! $close || $close['value'] !== ')') { throw new RuntimeException('Kurungan tidak seimbang dalam formula.'); } return $value; } if ($t['type'] === 'op' && in_array($t['value'], ['+', '-'], true)) { $op = $this->consume()['value']; $value = $this->parseFactor(); return $op === '-' ? -$value : $value; } if ($t['type'] === 'num') { $this->consume(); return (float) $t['value']; } if ($t['type'] === 'var') { $this->consume(); return $this->variables[$t['value']] ?? 0.0; } throw new RuntimeException('Token formula tidak dijangka: '.$t['value']); } }