前言
在公司产品中又新增了一个需求:需要在低代码工具平台中配置类似excel的函数表达式计算,如sum(单价*库存数量),其中“单价”和“库存数量”为该表格表头的标题,表示计算每一行的总金额然后对其求总和,之后显示到界面上显示。
一、难点分析
1.与excel表达式不同的是,需要开发的表达式中的变量并不是划定具体界限(如sum(A1, A2)),而是针对某一列或多列。
2.由于存在多列求和的情况,还需要使用方法解析出所有变量然后定位到表格上的表头。
3.真正复杂的表达式不止sum,还有平均、计数、最大值和最小值等,虽然当前只需要开发sum,但是也要考虑后期的扩展性。
二、开发流程
以下内容都以sum(price * number)为例
1.表达式变量解析
主要依赖QRegularExpression进行解析,然后利用chatgpt生成解析表达式:
QMap<QString, double> ExpressTool::extractVariables(const QString &runExpression) {
// 定义匹配变量名的正则表达式
QRegularExpression varRegex("\\b[a-zA-Z_][a-zA-Z0-9_]*\\b");
// 获取所有变量名
QRegularExpressionMatchIterator varIterator = varRegex.globalMatch(expression);
QMap<QString, double> variables;
while (varIterator.hasNext()) {
QRegularExpressionMatch match = varIterator.next();
QString varName = match.captured();
variables[varName] = 0.0; // 将变量映射到默认值0
}
return variables;
}
所有表达式变量,包括运算符都存到m_tokens中去(包括price 和number)
注意:以上runExpression为去掉sum()的表达式,也可利用chatgpt生成想要的解析方式
2.表达式运算符解析
主要解析该表达式为sum或其他,非常简单,按正常字符串解析即可:
QString expressSymple = expression.left(expression.indexOf("("));
if(expressSymple == "sum"){
//TODO sum
}
else{
//TODO other
}
3.表达式计算
这里为一大难点,需要找到变量对于表格数据的映射值。
在接口设计上,既要保证该代码的通用性,又要使接口简洁好用,因此设计出以下接口:
/**
* @brief 计算表达式(sum、avg)
* @param[in] express:表达式
* @param[in] rowCount:行总数
* @param[in] getValFunc:获取此行对应的值
* @retval 计算结果
*/
virtual double calcExpress(const QString &express, int rowCount, const std::function<void(int row, QMap<QString, double>&)>& getValFunc) = 0;
关键在于最后一个function中的(int row, QMap<QString, double>&),需要外部调用的代码手动给出QMap中某行对应key的数据,之后再传回内部进行计算:
double ExpressTool::evaluateExpression(const QString &runExpression, const QMap<QString, double> &variables) {
QString calcExpression = runExpression;
for(auto iter = variables.begin(); iter != variables.end(); ++iter){
QString regex = "\\b" + iter.key() + "\\b";
QRegularExpression re(regex);
calcExpression = calcExpression.replace(re, QString::number(iter.value()));
}
expression_t expressionTool;
parser_t parser;
parser.compile(calcExpression.toStdString(), expressionTool);
double resultVal = expressionTool.value();
return resultVal;
}
之后的每一行都进行如此计算,然后根据表达式运算符来判断是求和算法还是平均算法即可完成
注意: 1.在这里我使用了一个开源的数学计算代码exprtk,方便进行复杂的数学计算 2.替换文本时需要使用正则表达式确定完整的变量名,避免被某些不完整且有包含内容的变量名意外替换
三、应用举例
void MainWindow::on_pushButton_clicked()
{
QString express = ui->lineEdit->text();
auto tool = IExpressTool::getObj(this);
double ret = tool->calcExpress(express, ui->tableWidget->rowCount(), [this](int row, QMap<QString, double>& filed_m){
for(auto iter = filed_m.begin(); iter != filed_m.end(); ++iter){
int column = headerTitle.indexOf(iter.key());
double val = 0.0;
if(column != -1){
val = ui->tableWidget->item(row, column)->data(Qt::DisplayRole).toDouble();
}
iter.value() = val;
}
});
ui->doubleSpinBox->setValue(ret);
}
当点击计算按钮后,即可计算出结果显示到界面上: