2026-03-16 07:24:50 +07:00
|
|
|
<?php
|
|
|
|
|
|
2026-03-17 16:50:57 +07:00
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
|
|
|
|
|
|
|
|
|
class CalculatorService {
|
|
|
|
|
protected ExpressionLanguage $language;
|
2026-03-16 07:24:50 +07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gender mapping for calculations
|
|
|
|
|
* 0 = Unknown, 1 = Female, 2 = Male
|
|
|
|
|
*/
|
|
|
|
|
protected const GENDER_MAP = [
|
|
|
|
|
'unknown' => 0,
|
|
|
|
|
'female' => 1,
|
|
|
|
|
'male' => 2,
|
|
|
|
|
'0' => 0,
|
|
|
|
|
'1' => 1,
|
|
|
|
|
'2' => 2,
|
|
|
|
|
];
|
|
|
|
|
|
2026-03-17 16:50:57 +07:00
|
|
|
public function __construct() {
|
|
|
|
|
$this->language = new ExpressionLanguage();
|
|
|
|
|
}
|
2026-03-16 07:24:50 +07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate formula with variables
|
|
|
|
|
*
|
|
|
|
|
* @param string $formula Formula with placeholders like {result}, {factor}, {gender}
|
|
|
|
|
* @param array $variables Array of variable values
|
|
|
|
|
* @return float|null Calculated result or null on error
|
|
|
|
|
* @throws \Exception
|
|
|
|
|
*/
|
|
|
|
|
public function calculate(string $formula, array $variables = []): ?float {
|
2026-03-17 16:50:57 +07:00
|
|
|
try {
|
|
|
|
|
$normalizedFormula = $this->normalizeFormulaVariables($formula, $variables);
|
|
|
|
|
[$expression, $context] = $this->prepareExpression($normalizedFormula, $variables);
|
|
|
|
|
|
|
|
|
|
$result = $this->language->evaluate($expression, $context);
|
|
|
|
|
|
|
|
|
|
return (float) $result;
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
log_message('error', 'Calculator error: ' . $e->getMessage() . ' | Formula: ' . $formula);
|
|
|
|
|
throw new \Exception('Invalid formula: ' . $e->getMessage());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 07:24:50 +07:00
|
|
|
/**
|
|
|
|
|
* Validate formula syntax
|
|
|
|
|
*
|
|
|
|
|
* @param string $formula Formula to validate
|
|
|
|
|
* @return array ['valid' => bool, 'error' => string|null]
|
|
|
|
|
*/
|
|
|
|
|
public function validate(string $formula): array {
|
2026-03-17 16:50:57 +07:00
|
|
|
$formula = $this->normalizeFormulaVariables($formula, []);
|
|
|
|
|
$variables = array_fill_keys($this->extractVariables($formula), 1);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
[$expression, $context] = $this->prepareExpression($formula, $variables);
|
|
|
|
|
$this->language->evaluate($expression, $context);
|
|
|
|
|
|
|
|
|
|
return ['valid' => true, 'error' => null];
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
return ['valid' => false, 'error' => $e->getMessage()];
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-16 07:24:50 +07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract variable names from formula
|
|
|
|
|
*
|
|
|
|
|
* @param string $formula Formula with placeholders
|
|
|
|
|
* @return array List of variable names
|
|
|
|
|
*/
|
|
|
|
|
public function extractVariables(string $formula): array {
|
|
|
|
|
preg_match_all('/\{([^}]+)\}/', $formula, $matches);
|
|
|
|
|
return array_unique($matches[1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prepare expression by replacing placeholders with values
|
|
|
|
|
*/
|
2026-03-17 16:50:57 +07:00
|
|
|
protected function prepareExpression(string $formula, array $variables): array {
|
|
|
|
|
$expression = $formula;
|
|
|
|
|
$context = [];
|
|
|
|
|
$usedNames = [];
|
|
|
|
|
|
|
|
|
|
foreach ($variables as $key => $value) {
|
|
|
|
|
$placeholder = '{' . $key . '}';
|
|
|
|
|
|
|
|
|
|
if ($key === 'gender') {
|
|
|
|
|
$value = $this->normalizeGender($value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!is_numeric($value)) {
|
|
|
|
|
throw new \Exception("Variable '{$key}' must be numeric, got: " . var_export($value, true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$variableName = $this->getSafeVariableName($key, $usedNames);
|
|
|
|
|
$usedNames[] = $variableName;
|
|
|
|
|
$context[$variableName] = (float) $value;
|
|
|
|
|
|
|
|
|
|
$expression = str_replace($placeholder, $variableName, $expression);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (preg_match('/\{([^}]+)\}/', $expression, $unreplaced)) {
|
|
|
|
|
throw new \Exception("Missing variable value for: {$unreplaced[1]}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [$expression, $context];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function getSafeVariableName(string $key, array $usedNames): string {
|
|
|
|
|
$base = preg_replace('/[^A-Za-z0-9_]/', '_', (string) $key);
|
|
|
|
|
if ($base === '' || ctype_digit($base[0])) {
|
|
|
|
|
$base = 'v_' . $base;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$name = $base;
|
|
|
|
|
$suffix = 1;
|
|
|
|
|
while (in_array($name, $usedNames, true)) {
|
|
|
|
|
$name = $base . '_' . $suffix++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $name;
|
|
|
|
|
}
|
2026-03-16 07:24:50 +07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Normalize formulas that reference raw variable names instead of placeholders.
|
|
|
|
|
*/
|
|
|
|
|
protected function normalizeFormulaVariables(string $formula, array $variables): string
|
|
|
|
|
{
|
|
|
|
|
if (str_contains($formula, '{')) {
|
|
|
|
|
return $formula;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (empty($variables)) {
|
|
|
|
|
return $formula;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$keys = array_keys($variables);
|
|
|
|
|
usort($keys, fn($a, $b) => mb_strlen($b) <=> mb_strlen($a));
|
|
|
|
|
|
|
|
|
|
foreach ($keys as $key) {
|
|
|
|
|
$escaped = preg_quote($key, '/');
|
|
|
|
|
$formula = preg_replace_callback('/\b' . $escaped . '\b/i', function () use ($key) {
|
|
|
|
|
return '{' . $key . '}';
|
|
|
|
|
}, $formula);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $formula;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Normalize gender value to numeric (0, 1, or 2)
|
|
|
|
|
*/
|
|
|
|
|
protected function normalizeGender($gender): int {
|
|
|
|
|
if (is_numeric($gender)) {
|
|
|
|
|
$num = (int) $gender;
|
|
|
|
|
return in_array($num, [0, 1, 2], true) ? $num : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$genderLower = strtolower((string) $gender);
|
|
|
|
|
return self::GENDER_MAP[$genderLower] ?? 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate from TestDefCal record
|
|
|
|
|
*
|
|
|
|
|
* @param array $calcDef Test calculation definition
|
|
|
|
|
* @param array $testValues Test result values
|
|
|
|
|
* @return float|null
|
|
|
|
|
*/
|
|
|
|
|
public function calculateFromDefinition(array $calcDef, array $testValues): ?float {
|
|
|
|
|
$formula = $calcDef['FormulaCode'] ?? '';
|
|
|
|
|
|
|
|
|
|
if (empty($formula)) {
|
|
|
|
|
throw new \Exception('No formula defined');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build variables array
|
|
|
|
|
$variables = [
|
|
|
|
|
'result' => $testValues['result'] ?? 0,
|
|
|
|
|
'factor' => $calcDef['Factor'] ?? 1,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Add optional variables
|
|
|
|
|
if (isset($testValues['gender'])) {
|
|
|
|
|
$variables['gender'] = $testValues['gender'];
|
|
|
|
|
}
|
|
|
|
|
if (isset($testValues['age'])) {
|
|
|
|
|
$variables['age'] = $testValues['age'];
|
|
|
|
|
}
|
|
|
|
|
if (isset($testValues['ref_low'])) {
|
|
|
|
|
$variables['ref_low'] = $testValues['ref_low'];
|
|
|
|
|
}
|
|
|
|
|
if (isset($testValues['ref_high'])) {
|
|
|
|
|
$variables['ref_high'] = $testValues['ref_high'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Merge any additional test values
|
|
|
|
|
$variables = array_merge($variables, $testValues);
|
|
|
|
|
|
|
|
|
|
return $this->calculate($formula, $variables);
|
|
|
|
|
}
|
|
|
|
|
}
|