实现数学四则运算功能

需求概括

后台录入最小值,最大值公式,前台筛选时如果满足条件则进行公式计算

解决思路

公式的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.两个堆栈,一个用来存储数字,一个用来存储运算符,遇到括号以后就递归进入括号内运算

参考

用PHP实现的四则运算表达式计算

PHP 实现后缀表达式(接受四则运算字符串,输出计算结果,附代码)

结合正则表达式验证数学公式(含变量,js版)