first commit

This commit is contained in:
Saufi
2026-06-24 20:32:14 +08:00
commit 10fb30ad69
201 changed files with 21356 additions and 0 deletions

View File

@@ -0,0 +1,204 @@
<?php
namespace App\Services;
use RuntimeException;
/**
* Penilai ungkapan aritmetik yang selamat (recursive-descent parser).
*
* Menyokong: + - * / ( ), nombor perpuluhan, dan pemboleh ubah (token) yang
* dipetakan daripada array $variables. Tiada eval()/PHP code dilaksanakan,
* jadi selamat untuk formula yang dikonfigur oleh pengguna JPP.
*/
class ExpressionEvaluator
{
/** @var array<int, array{type:string, value:string}> */
protected array $tokens = [];
protected int $pos = 0;
/** @var array<string, float> */
protected array $variables = [];
protected bool $guardDivZero = false;
protected bool $divByZeroHit = false;
/**
* @param array<string, float|int|null> $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<int, array{type:string, value:string}>
*/
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']);
}
}