2, '-' => 2, '*' => 3, '/' => 3, '%' => 3, 'UNARY_MINUS' => 4, ]; /** * 산술 수식을 계산하여 float 반환 */ public static function calculate(string $expression): float { $expression = trim($expression); if ($expression === '') { return 0; } $tokens = self::tokenize($expression); if (empty($tokens)) { return 0; } $rpn = self::toRPN($tokens); return self::evaluateRPN($rpn); } /** * 비교식을 평가하여 bool 반환 * 예: "3000 <= 6000", "100 == 100", "5 > 3 && 2 < 4" */ public static function compare(string $expression): bool { $expression = trim($expression); // && 논리 AND 처리 if (str_contains($expression, '&&')) { $parts = explode('&&', $expression); foreach ($parts as $part) { if (! self::compare(trim($part))) { return false; } } return true; } // || 논리 OR 처리 if (str_contains($expression, '||')) { $parts = explode('||', $expression); foreach ($parts as $part) { if (self::compare(trim($part))) { return true; } } return false; } // 비교 연산자 추출 (2문자 먼저 검사) $operators = ['>=', '<=', '!=', '==', '>', '<']; foreach ($operators as $op) { $pos = strpos($expression, $op); if ($pos !== false) { $left = self::calculate(substr($expression, 0, $pos)); $right = self::calculate(substr($expression, $pos + strlen($op))); return match ($op) { '>=' => $left >= $right, '<=' => $left <= $right, '!=' => $left != $right, '==' => $left == $right, '>' => $left > $right, '<' => $left < $right, }; } } // 비교 연산자가 없으면 수치를 boolean으로 평가 return (bool) self::calculate($expression); } /** * 수식 문자열을 토큰 배열로 분리 */ private static function tokenize(string $expression): array { $tokens = []; $len = strlen($expression); $i = 0; while ($i < $len) { $char = $expression[$i]; // 공백 건너뛰기 if ($char === ' ' || $char === "\t") { $i++; continue; } // 숫자 (정수, 소수) if (is_numeric($char) || ($char === '.' && $i + 1 < $len && is_numeric($expression[$i + 1]))) { $num = ''; while ($i < $len && (is_numeric($expression[$i]) || $expression[$i] === '.')) { $num .= $expression[$i]; $i++; } $tokens[] = ['type' => 'number', 'value' => (float) $num]; continue; } // 괄호 if ($char === '(') { $tokens[] = ['type' => 'lparen']; $i++; continue; } if ($char === ')') { $tokens[] = ['type' => 'rparen']; $i++; continue; } // 연산자 if (in_array($char, self::OPERATORS)) { // 단항 마이너스 판별: 맨 앞이거나, 앞이 연산자 또는 여는 괄호인 경우 if ($char === '-') { $isUnary = empty($tokens) || $tokens[count($tokens) - 1]['type'] === 'operator' || $tokens[count($tokens) - 1]['type'] === 'lparen'; if ($isUnary) { $tokens[] = ['type' => 'operator', 'value' => 'UNARY_MINUS']; $i++; continue; } } $tokens[] = ['type' => 'operator', 'value' => $char]; $i++; continue; } throw new InvalidArgumentException("허용되지 않는 문자: '{$char}' (위치 {$i})"); } return $tokens; } /** * 중위 표기법 토큰 → 후위 표기법(RPN) 변환 (Shunting-yard) */ private static function toRPN(array $tokens): array { $output = []; $operatorStack = []; foreach ($tokens as $token) { if ($token['type'] === 'number') { $output[] = $token; continue; } if ($token['type'] === 'operator') { $op = $token['value']; $prec = self::PRECEDENCE[$op] ?? 0; while (! empty($operatorStack)) { $top = end($operatorStack); if ($top['type'] === 'lparen') { break; } $topPrec = self::PRECEDENCE[$top['value']] ?? 0; // 단항 연산자는 오른쪽 결합 if ($op === 'UNARY_MINUS') { if ($topPrec > $prec) { $output[] = array_pop($operatorStack); } else { break; } } else { // 이항 연산자는 왼쪽 결합 (같은 우선순위면 먼저 pop) if ($topPrec >= $prec) { $output[] = array_pop($operatorStack); } else { break; } } } $operatorStack[] = $token; continue; } if ($token['type'] === 'lparen') { $operatorStack[] = $token; continue; } if ($token['type'] === 'rparen') { while (! empty($operatorStack) && end($operatorStack)['type'] !== 'lparen') { $output[] = array_pop($operatorStack); } if (empty($operatorStack)) { throw new InvalidArgumentException('괄호 불일치: 여는 괄호 없음'); } array_pop($operatorStack); // 여는 괄호 제거 continue; } } while (! empty($operatorStack)) { $top = array_pop($operatorStack); if ($top['type'] === 'lparen') { throw new InvalidArgumentException('괄호 불일치: 닫는 괄호 없음'); } $output[] = $top; } return $output; } /** * 후위 표기법(RPN) 계산 */ private static function evaluateRPN(array $rpn): float { $stack = []; foreach ($rpn as $token) { if ($token['type'] === 'number') { $stack[] = $token['value']; continue; } if ($token['type'] === 'operator') { $op = $token['value']; // 단항 마이너스 if ($op === 'UNARY_MINUS') { if (empty($stack)) { throw new InvalidArgumentException('수식 오류: 단항 마이너스 피연산자 없음'); } $stack[] = -array_pop($stack); continue; } // 이항 연산자 if (count($stack) < 2) { throw new InvalidArgumentException('수식 오류: 피연산자 부족'); } $right = array_pop($stack); $left = array_pop($stack); $stack[] = match ($op) { '+' => $left + $right, '-' => $left - $right, '*' => $left * $right, '/' => $right != 0 ? $left / $right : throw new InvalidArgumentException('0으로 나눌 수 없음'), '%' => $right != 0 ? fmod($left, $right) : throw new InvalidArgumentException('0으로 나눌 수 없음'), default => throw new InvalidArgumentException("알 수 없는 연산자: {$op}"), }; } } if (count($stack) !== 1) { throw new InvalidArgumentException('수식 오류: 결과가 하나가 아님'); } return (float) $stack[0]; } }