false, 'errors' => [__('error.formula_empty')], ]; } // 괄호 매칭 검증 if (! $this->validateParentheses($formula)) { $errors[] = __('error.formula_parentheses_mismatch'); } // 변수 추출 $variables = $this->extractVariables($formula); // 지원 함수 검증 $functions = $this->extractFunctions($formula); foreach ($functions as $func) { if (! in_array(strtoupper($func), self::SUPPORTED_FUNCTIONS)) { $errors[] = __('error.formula_unsupported_function', ['function' => $func]); } } return [ 'success' => empty($errors), 'errors' => $errors, 'variables' => $variables, 'functions' => $functions, ]; } /** * 수식 평가 * * @param string $formula 수식 문자열 * @param array $variables 변수 배열 [변수명 => 값] */ public function evaluate(string $formula, array $variables = []): mixed { $this->variables = array_merge($this->variables, $variables); $this->errors = []; try { // 변수 치환 $expression = $this->substituteVariables($formula); // 함수 처리 $expression = $this->processFunctions($expression); // 최종 계산 return $this->calculateExpression($expression); } catch (\Exception $e) { $this->errors[] = $e->getMessage(); return null; } } /** * 다중 수식 일괄 평가 * * @param array $formulas [변수명 => 수식] 배열 * @param array $inputVariables 입력 변수 * @return array [변수명 => 결과값] */ public function evaluateMultiple(array $formulas, array $inputVariables = []): array { $this->variables = $inputVariables; $results = []; foreach ($formulas as $variable => $formula) { $result = $this->evaluate($formula); $this->variables[$variable] = $result; $results[$variable] = $result; } return [ 'results' => $results, 'errors' => $this->errors, ]; } /** * 범위 기반 값 결정 * * @param float $value 검사할 값 * @param array $ranges 범위 배열 [['min' => 0, 'max' => 100, 'result' => 값], ...] * @param mixed $default 기본값 */ public function evaluateRange(float $value, array $ranges, mixed $default = null): mixed { foreach ($ranges as $range) { $min = $range['min'] ?? null; $max = $range['max'] ?? null; $inRange = true; if ($min !== null && $value < $min) { $inRange = false; } if ($max !== null && $value > $max) { $inRange = false; } if ($inRange) { $result = $range['result'] ?? $default; // result가 수식인 경우 평가 if (is_string($result) && $this->isFormula($result)) { return $this->evaluate($result); } return $result; } } return $default; } /** * 매핑 기반 값 결정 * * @param mixed $sourceValue 소스 값 * @param array $mappings 매핑 배열 [['source' => 값, 'result' => 결과], ...] * @param mixed $default 기본값 */ public function evaluateMapping(mixed $sourceValue, array $mappings, mixed $default = null): mixed { foreach ($mappings as $mapping) { $source = $mapping['source'] ?? null; $result = $mapping['result'] ?? $default; if ($sourceValue == $source) { // result가 수식인 경우 평가 if (is_string($result) && $this->isFormula($result)) { return $this->evaluate($result); } return $result; } } return $default; } /** * 괄호 매칭 검증 */ private function validateParentheses(string $formula): bool { $count = 0; foreach (str_split($formula) as $char) { if ($char === '(') { $count++; } if ($char === ')') { $count--; } if ($count < 0) { return false; } } return $count === 0; } /** * 수식에서 변수 추출 */ private function extractVariables(string $formula): array { preg_match_all('/\b([A-Z][A-Z0-9_]*)\b/', $formula, $matches); $variables = array_unique($matches[1] ?? []); // 함수명 제외 return array_values(array_diff($variables, self::SUPPORTED_FUNCTIONS)); } /** * 수식에서 함수 추출 */ private function extractFunctions(string $formula): array { preg_match_all('/\b([A-Za-z_]+)\s*\(/', $formula, $matches); return array_unique($matches[1] ?? []); } /** * 변수 치환 */ private function substituteVariables(string $formula): string { foreach ($this->variables as $var => $value) { $formula = preg_replace('/\b'.preg_quote($var, '/').'\b/', (string) $value, $formula); } return $formula; } /** * 함수 처리 */ private function processFunctions(string $expression): string { // ROUND(value, decimals) $expression = preg_replace_callback( '/ROUND\s*\(\s*([^,]+)\s*,\s*(\d+)\s*\)/i', fn ($m) => round((float) $this->calculateExpression($m[1]), (int) $m[2]), $expression ); // SUM(a, b, c, ...) $expression = preg_replace_callback( '/SUM\s*\(([^)]+)\)/i', fn ($m) => array_sum(array_map('floatval', explode(',', $m[1]))), $expression ); // MIN, MAX $expression = preg_replace_callback( '/MIN\s*\(([^)]+)\)/i', fn ($m) => min(array_map('floatval', explode(',', $m[1]))), $expression ); $expression = preg_replace_callback( '/MAX\s*\(([^)]+)\)/i', fn ($m) => max(array_map('floatval', explode(',', $m[1]))), $expression ); // IF(condition, true_val, false_val) $expression = preg_replace_callback( '/IF\s*\(\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^)]+)\s*\)/i', function ($m) { $condition = $this->evaluateCondition($m[1]); return $condition ? $this->calculateExpression($m[2]) : $this->calculateExpression($m[3]); }, $expression ); // ABS, CEIL, FLOOR $expression = preg_replace_callback( '/ABS\s*\(([^)]+)\)/i', fn ($m) => abs((float) $this->calculateExpression($m[1])), $expression ); $expression = preg_replace_callback( '/CEIL\s*\(([^)]+)\)/i', fn ($m) => ceil((float) $this->calculateExpression($m[1])), $expression ); $expression = preg_replace_callback( '/FLOOR\s*\(([^)]+)\)/i', fn ($m) => floor((float) $this->calculateExpression($m[1])), $expression ); return $expression; } /** * 수식 계산 (안전한 평가) */ private function calculateExpression(string $expression): float { // 안전한 수식 평가 (숫자, 연산자, 괄호만 허용) $expression = preg_replace('/[^0-9+\-*\/().%\s]/', '', $expression); if (empty(trim($expression))) { return 0; } try { // TODO: 프로덕션에서는 symfony/expression-language 등 안전한 라이브러리 사용 권장 return (float) eval("return {$expression};"); } catch (\Throwable $e) { $this->errors[] = __('error.formula_calculation_error', ['expression' => $expression]); return 0; } } /** * 조건식 평가 */ private function evaluateCondition(string $condition): bool { // 비교 연산자 처리 if (preg_match('/(.+)(>=|<=|>|<|==|!=)(.+)/', $condition, $m)) { $left = (float) $this->calculateExpression(trim($m[1])); $right = (float) $this->calculateExpression(trim($m[3])); $op = $m[2]; return match ($op) { '>=' => $left >= $right, '<=' => $left <= $right, '>' => $left > $right, '<' => $left < $right, '==' => $left == $right, '!=' => $left != $right, default => false, }; } return (bool) $this->calculateExpression($condition); } /** * 문자열이 수식인지 확인 */ private function isFormula(string $value): bool { // 연산자나 함수가 포함되어 있으면 수식으로 판단 return preg_match('/[+\-*\/()]|[A-Z]+\s*\(/', $value) === 1; } /** * 에러 목록 반환 */ public function getErrors(): array { return $this->errors; } /** * 현재 변수 상태 반환 */ public function getVariables(): array { return $this->variables; } /** * 변수 설정 */ public function setVariables(array $variables): self { $this->variables = $variables; return $this; } /** * 변수 추가 */ public function addVariable(string $name, mixed $value): self { $this->variables[$name] = $value; return $this; } /** * 변수 및 에러 초기화 */ public function reset(): void { $this->variables = []; $this->errors = []; } }