需求概括
后台录入最小值,最大值公式,前台筛选时如果满足条件则进行公式计算
解决思路
公式的CRUD
数据库保存公式时,公式中的参数用{参数id}替代,例如公式为P/1+2,P的id为5 则公式写作{5}/1+2
查询后将参数id替换成具体参数名,删除时,将包含{id}的公式字段清空
公式验证
验证公式时,先解析出{id}的参数,验证数据是否存在,然后在验证公式是否合法
/**
* 解析公式表达式中的参数
*
* @param string $formula 公式表达式
* @return mixed array|bool
*/
public function parseParamsIds(string $formula = '')
{
$value = $formula ? $formula : $this->formula_exp;
preg_match_all('/\{(\d+)\}/', $value, $matches);
if (! isset($matches[1]) || count($matches[1]) === 0) {
$this->error_message = '公式中缺少数字类型产品参数';
return false;
}
return $matches[1];
}
/**
* 公式验证
* @param string $formula_exp 公式表达式
* @param array $params 公式参数
* @return bool
*/
public function validateLegitimacy()
{
$formula_exp = trim($this->formula_exp);
//为空
if ($formula_exp === '') {
$this->error_message = '公式为空';
return false;
}
//错误情况,运算符连续
if (preg_match_all('/[\+\-\*\/]{2,}/', $formula_exp)) {
$this->error_message = '公式错误,运算符连续';
return false;
}
//空括号
if (preg_match_all('/\(\)/', $formula_exp)) {
$this->error_message = '公式错误,存在空括号';
return false;
}
//错误情况,(后面是运算符
if (preg_match_all('/\([\+\-\*\/]/', $formula_exp)) {
$this->error_message = '公式错误,(后面是运算符';
return false;
}
// 错误情况,)前面是运算符
if (preg_match_all('/[\+\-\*\/]\)/', $formula_exp)) {
$this->error_message = '公式错误,)前面是运算符';
return false;
}
//错误情况,(前面不是运算符
if (preg_match_all('/[^\+\-\*\/]\(/', $formula_exp)) {
$this->error_message = '公式错误,(前面不是运算符';
return false;
}
//错误情况,)后面不是运算符
if (preg_match_all('/\)[^\+\-\*\/]/', $formula_exp)) {
$this->error_message = '公式错误,)后面不是运算符';
return false;
}
//错误情况,使用除()+-*/之外的字符
if (preg_match_all('/[^\+\-\*\/0-9.a-zA-Z\(\)]/', $formula_exp)) {
$this->error_message = '公式错误,使用除()+-*/之外的字符';
return false;
}
//运算符号不能在首末位
if (preg_match_all('/^[\+\-\*\/.]|[\+\-\*\/.]$/', $formula_exp)) {
$this->error_message = '公式错误,运算符号不能在首末位';
return false;
}
//错误情况,括号不配对
$str_len = strlen($formula_exp);
$stack = [];
for ($i = 0; $i < $str_len; $i++) {
$item = $formula_exp[$i];
if ($item === '(') {
array_push($stack, $item);
} elseif ($item === ')') {
if (count($stack) > 0) {
array_pop($stack);
} else {
$this->error_message = '公式错误,括号不配对';
return false;
}
}
}
if (count($stack) > 0) {
$this->error_message = '公式错误,括号不配对';
return false;
}
//错误情况,变量没有来自“待选公式变量”
$arr = preg_split('/[\(\)\+\-\*\/]{1,}/', $formula_exp);
foreach ($arr as $key => $value) {
if (preg_match_all('/[A-Z]/i', $value) && ! isset($this->params[$value])) {
$this->error_message = '公式错误,参数不配';
return false;
}
}
return true;
}
公式计算方案
1.逆波兰表达式也叫后缀表达
class FormulaCalculate
{
//正则表达式,用于将表达式字符串,解析为单独的运算符和操作项
public const PATTERN_EXP = '/((?:[a-zA-Z0-9_]+)|(?:[\(\)\+\-\*\/])){1}/';
public const EXP_PRIORITIES = ['+' => 1, '-' => 1, '*' => 2, '/' => 2, '(' => 0, ')' => 0];
/**
* 公式计算
*
* @param string $exp 普通表达式,例如 a+b*(c+d)
* @param array $exp_values 表达式对应数据内容,例如 ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]
* @return int
*/
public static function calculate($exp, $exp_values)
{
$exp_arr = self::parseExp($exp); //将表达式字符串解析为列表
if (! is_array($exp_arr)) {
return 0;
}
$output_queue = self::nifix2rpn($exp_arr);
return self::calculateValue($output_queue, $exp_values);
}
/**
* 将字符串中每个操作项和预算符都解析出来
*
* @param string $exp 普通表达式
* @return mixed
*/
protected static function parseExp($exp)
{
$match = [];
preg_match_all(self::PATTERN_EXP, $exp, $match);
if ($match) {
return $match[0];
} else {
return null;
}
}
/**
* 将中缀表达式转为后缀表达式
*
* @param array $input_queue 输入队列
* @return array
*/
protected static function nifix2rpn($input_queue)
{
$exp_stack = [];
$output_queue = [];
foreach ($input_queue as $input) {
if (in_array($input, array_keys(self::EXP_PRIORITIES))) {
if ($input == '(') {
array_push($exp_stack, $input);
continue;
}
if ($input == ')') {
$tmp_exp = array_pop($exp_stack);
while ($tmp_exp && $tmp_exp != '(') {
array_push($output_queue, $tmp_exp);
$tmp_exp = array_pop($exp_stack);
}
continue;
}
foreach (array_reverse($exp_stack) as $exp) {
if (self::EXP_PRIORITIES[$input] <= self::EXP_PRIORITIES[$exp]) {
array_pop($exp_stack);
array_push($output_queue, $exp);
} else {
break;
}
}
array_push($exp_stack, $input);
} else {
array_push($output_queue, $input);
}
}
foreach (array_reverse($exp_stack) as $exp) {
array_push($output_queue, $exp);
}
return $output_queue;
}
/**
* 传入后缀表达式队列、各项对应值的数组,计算出结果
*
* @param array $output_queue 后缀表达式队列
* @param array $exp_values 表达式对应数据内容
* @return mixed
*/
protected static function calculateValue($output_queue, $exp_values)
{
$res_stack = [];
foreach ($output_queue as $out) {
if (in_array($out, array_keys(self::EXP_PRIORITIES))) {
$a = array_pop($res_stack);
$b = array_pop($res_stack);
switch ($out) {
case '+':
$res = $b + $a;
break;
case '-':
$res = $b - $a;
break;
case '*':
$res = $b * $a;
break;
case '/':
$res = $b / $a;
break;
}
array_push($res_stack, $res);
} else {
if (is_numeric($out)) {
array_push($res_stack, intval($out));
} else {
array_push($res_stack, $exp_values[$out]);
}
}
}
return count($res_stack) == 1 ? $res_stack[0] : null;
}
}
2.使用eval()
运行代码,要注意代码安全性,做特别严格的验证
数据用例,前台输入值P 输入5 ,L输入6,
$eval = '$result = ';
//计算公式
$formula = '{P}/2+{L}*3+23';
//接收输入值
$params = ['{P}','{L}'];
$input = [5,6];
$eval .= str_replace($params, $input, $formula);
$eval .= ';';
eval($eval);
echo $result,PHP_EOL;
3.两个堆栈,一个用来存储数字,一个用来存储运算符,遇到括号以后就递归进入括号内运算
参考