首页>>前端>>JavaScript->使用 js 实现加减乘除模板计算

使用 js 实现加减乘除模板计算

时间:2023-11-29 本站 点击:0

最近接到了一个新需求,要求实现一个动态的录入表单,这个表单绝大多数都是数字录入,并且其中的某些字段是有关联关系的。例如三个字段 field1field2field3,它们的关系可能为:field3 = field1 + field2。到时候后端会提供给你这个表达式,前端来自动计算。

你可能会想,没啥问题,监听 form 值变更,依赖字段变化了就把这个表达式里的字段名字符串替换成实际值,最后 eval 一下不就好了。我一开始也确实是这么做的,但是 js 的精度问题给了我一脚:

如果两个字段的值分别为 0.1 和 0.2,最后计算的结果将会为 0.30000000000000004。这在这种数字表单录入里是不能接收的,单纯的乘 100 最后除 100 也不靠谱,加上 eval 本身的不安全性,所以路还是要一步步的走。

现在回看上面的描述,我们可以把问题整理成下面这道题(完整实现见文末):

/***实现一个函数,接收表达式模板和字段值,计算最终结果*/consttemplateCalc=(template,values)=>{//...}templateCalc('(val1+val2)/val3',{val1:1,val2:2,val3:2});//1.5templateCalc('((val1+val2)-val3*val4)/val3',{val1:1,val2:2,val3:2,val4:10});//-8.5templateCalc('val1-val2',{val1:0.3,val2:0.2});//0.1templateCalc('(val1+val2)/10000',{val1:100,val2:5});//0.0105

在精度问题上,我选择了 big.js - npm (npmjs.com) 来处理。处理的整体思路如下:

把模板中的字段名和操作符拆开,即将字符串解析为 token 数组

把 token 数组处理成逆波兰表达式

计算逆波兰表达式时将字段名替换为实际值并引入 big.js 计算

ok,接下来搞第一步:

解析模板

实现如下:

//匹配加减乘除括号的正则constoperatorReg=/[()+\-/*]/g;/***将模板处理为token数组*@param{string}str要计算的表达式模板*/exportconststrToToken=str=>{//提取出所有操作数constkeys=str.split(operatorReg);consttokens=[];lettemp=str;//解析模板while(temp.length>0){//模板开头是操作数if(keys.length>0&&temp.startsWith(keys[0])){temp=temp.replace(keys[0],'');tokens.push(keys.shift());}//模板开头是操作符else{tokens.push(temp[0]);temp=temp.substr(1);}}//把模板里的空白字符都丢掉returntokens.filter(token=>token&&token!=='');}

这里取了个巧,没有直接去解析模板,而且先用操作符把模板分隔开,这样剩下的就是操作数了,然后拿操作数去头部匹配模板,匹配上了就把这个操作数扔到最后队列里,没匹配上就说明是个操作符,把第一个字符扔队列里。

转换为逆波兰表达式

实现如下:

//匹配加减乘除括号的正则constoperatorReg=/[()+\-/*]/g;/***中缀表达式转换成逆波兰表达式*@param{string[]}tokenList中缀表达式token数组*/consttokenToRpn=tokenList=>{if(!tokenList||tokenList.length<=0)return[];constoperators=[];//指定操作符优先级是否高于栈中操作符的优先级constisTokenHighRank=token=>{constoperatorRand={'+':1,'-':1,'*':2,'/':2};consttopOperator=operators[operators.length-1];returnoperators.length===0||topOperator==='('||operatorRand[token]>operatorRand[topOperator];}constoutputs=tokenList.reduce((outputs,token)=>{//如果是变量,直接输出if(!token.match(operatorReg))outputs.push(token);//如果是左括号,入操作符栈elseif(token==='(')operators.push(token);//如果是右括号,把操作符弹出到遇见左括号elseif(token===')'){while(operators.length>0){constoperator=operators.pop();if(operator==='(')break;outputs.push(operator);}}//如果是运算符else{while(operators.length>=0){//把优先级更高的推入结果if(isTokenHighRank(token)){operators.push(token);break;}outputs.push(operators.pop());}}returnoutputs;},[]);return[...outputs,...operators];}

里边要注意前后括号的匹配和操作符优先级问题。这个实现感觉还是有点长了,如果有更好的转换方法请务必告诉我。

替换数据并计算

实现如下:

constBig=require('big.js');/***运算符到实际操作的映射*/constcalculators={'+':(num1,num2)=>(newBig(num1).plus(num2)),'-':(num1,num2)=>(newBig(num1).minus(num2)),'*':(num1,num2)=>(newBig(num1).times(num2)),'/':(num1,num2)=>(newBig(num1).div(num2))}/***从数据集里获取对应的数据*/constgetValues=(key,values)=>{if(!key)return0;if(typeofkey==='string')returnvalues[key]||Number(key)||0;returnkey;}/***填充并计算数据*@param{string[]}tokensrpntoken数组*@param{object}values数据集*@returns最终结果*/constcalcRpn=(tokens,values)=>{letnumarr=[]for(consttokenoftokens){constcalculator=calculators[token];if(!calculator)numarr.push(token);else{//这两个值的创建顺序不能变,否则pop出来的值就反了constval2=getValues(numarr.pop(),values);constval1=getValues(numarr.pop(),values);constresult=calculator(val1,val2);numarr.push(result.toNumber());}}returnnumarr.pop();};

这里需要注意的是,getValues 第一个参数接收的值是从计算栈 numarr 中弹出来的,所以它有可能是一个字段名 field1,也有可能是已经计算过的实际数值。所以需要判断其类型,这里还有一点需要注意的是,当 key 参数值的类型为 string 时,不仅代表其有可能为字段名,还有可能为一个模板中的操作数。

完整示例

这三步完成之后剩下的就好办了,把它们仨依次串联起来就可以了,下面是完整示例,丢给 node 就能跑:

constBig=require('big.js');//匹配加减乘除括号的正则constoperatorReg=/[()+\-/*]/g;/***将模板处理为token数组*@param{string}str要计算的表达式模板*/conststrToToken=str=>{//提取出所有操作数constkeys=str.split(operatorReg);consttokens=[];lettemp=str;//解析模板while(temp.length>0){//模板开头是操作数if(keys.length>0&&temp.startsWith(keys[0])){temp=temp.replace(keys[0],'');tokens.push(keys.shift());}//模板开头是操作符else{tokens.push(temp[0]);temp=temp.substr(1);}}//把模板里的空白字符都丢掉returntokens.filter(token=>token&&token!=='');}/***中缀表达式转换成逆波兰表达式*@param{string[]}tokenList中缀表达式token数组*/consttokenToRpn=tokenList=>{if(!tokenList||tokenList.length<=0)return[];constoperators=[];//指定操作符优先级是否高于栈中操作符的优先级constisTokenHighRank=token=>{constoperatorRand={'+':1,'-':1,'*':2,'/':2};consttopOperator=operators[operators.length-1];returnoperators.length===0||topOperator==='('||operatorRand[token]>operatorRand[topOperator];}constoutputs=tokenList.reduce((outputs,token)=>{//如果是变量,直接输出if(!token.match(operatorReg))outputs.push(token);//如果是左括号,入操作符栈elseif(token==='(')operators.push(token);//如果是右括号,把操作符弹出到遇见左括号elseif(token===')'){while(operators.length>0){constoperator=operators.pop();if(operator==='(')break;outputs.push(operator);}}//如果是运算符else{while(operators.length>=0){//把优先级更高的推入结果if(isTokenHighRank(token)){operators.push(token);break;}outputs.push(operators.pop());}}returnoutputs;},[]);return[...outputs,...operators];}/***运算符到实际操作的映射*/constcalculators={'+':(num1,num2)=>(newBig(num1).plus(num2)),'-':(num1,num2)=>(newBig(num1).minus(num2)),'*':(num1,num2)=>(newBig(num1).times(num2)),'/':(num1,num2)=>(newBig(num1).div(num2))}/***从数据集里获取对应的数据*/constgetValues=(key,values)=>{if(!key)return0;if(typeofkey==='string')returnvalues[key]||Number(key)||0;returnkey;}/***填充并计算数据*@param{string[]}tokensrpntoken数组*@param{object}values数据集*@returns最终结果*/constcalcRpn=function(tokens,values){letnumarr=[]for(consttokenoftokens){constcalculator=calculators[token];if(!calculator)numarr.push(token);else{//这两个值的创建顺序不能变,否则pop出来的值就反了constval2=getValues(numarr.pop(),values);constval1=getValues(numarr.pop(),values);constresult=calculator(val1,val2);numarr.push(result.toNumber());}}returnnumarr.pop();};consttemplateCalc=(template,values)=>{consttokens=strToToken(template)constrpn=tokenToRpn(tokens)constresult=calcRpn(rpn,values)returnresult}console.log(templateCalc('(val1+val2)/val3',{val1:1,val2:2,val3:2}));//1.5console.log(templateCalc('((val1+val2)-val3*val4)/val3',{val1:1,val2:2,val3:2,val4:10}));//-8.5console.log(templateCalc('val1-val2',{val1:0.3,val2:0.2}));//0.1console.log(templateCalc('(val1+val2)/10000',{val1:100,val2:5}));//0.0105


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/JavaScript/716.html