介绍
要实现数学表达式计算器,波及到两个办法,波兰表示法(维基百科)和逆波兰表示法(维基百科),具体的概念能够自行查看百科。
波兰表达式是将操作符前置,也就是前缀表达式;逆波兰表达式是将操作符后置,即后缀表达式。而咱们失常去输出的叫中断表达式,要是间接去对中断表达式进行计算,那么优先级的判断就不是很不便,而前缀或者后缀就很不便去写计算逻辑。
成果展现
- 在线Demo
- 代码地址
实现思路
转换表达式
所以首要任务是将输出的数学表达式进行转换,在这里我抉择的是将中断转换成后缀表达式来计算,当然用前缀也一样。
咱们须要申明两个栈,一个用来存符号,一个用来存输入的后缀表达式。
const stack: string[] = [];const output: string[] = [];
先解决输出的表达式字符串,将其宰割为数字和符号两种,都存在同一个数组中。这里我将-
(减号)替换成+
号解决,不便前面做计算。
split 反对应用正则表达式去宰割字符串,这里就是用操作符去宰割字符串,须要留神的是,得加上将正则匹配项加上(),split 才会收集用来宰割的操作符。
const reg = new RegExp(/(\+|\*|x|\/|\(|\))/g);const list = value.replace(/\-/g, "+-").split(reg);
- 符号优先级
用一个对象去记录符号的优先级,不便转换的时候判断是符号还是数字,以及入栈的程序。
优先级 ) < 加减 < 乘除 < (
这里多加了 x
是因为我想用x示意乘法,打字时少按一个键
export const Single = { "+": 1, "-": 1, "*": 2, x: 2, "/": 2, "(": 3, ")": -1,};
- 外围转换逻辑
便当咱们解决好的list,我没有提前对空格做解决,因为我不想先用filter便当一遍,所以在转换得时候得去判断一下,如果 str
为空,跳过此次循环;
str
有两种状况,数字或者是操作符:
数字很简略,间接 push
到 output
(最终的表达式)中;
如果是操作符,就要分状况解决了。
- 当栈顶没有元素时,间接 push 到
stack
操作符栈中 - 如果以后操作符的优先级大于栈顶元素,或者以后是左括号操作符,间接推到符号栈,因为我用pop获取的栈顶元素,因而还得把栈顶元素push回去
如果以后操作符的优先级小于栈顶元素,则把栈顶元素push到表达式栈中,而后持续获取栈顶元素
- 当栈顶元素是
(
并且以后操作符不是)
,须要把(
push回去,保障括号内表达式的优先级 - 当遇到
)
时,(
就被pop掉了
- 当栈顶元素是
这里没有间接去判断str是否等于 ()
,因为咱们提前做了优先级的设定的
(
的优先级最高,会被间接推入栈中; )
的优先级最低,遇到时会把左括号后的操作符解决掉,并且做了 index>0
的限度,右括号不会入栈。
for (let i = 0; i < list.length; i++) { const str = list[i]; if (!str) continue; const index = Single[str]; if (!!index) { let topValue = stack.pop() || ""; let topIndex = Single[topValue]; if (topIndex === undefined) { stack.push(str); continue; } if (index > topIndex || topIndex === 3) { stack.push(topValue); stack.push(str); continue; } else { while (topIndex && index <= topIndex) { if (topIndex < 3) output.push(topValue); topValue = stack.pop() || ""; topIndex = Single[topValue]; // 解决括号 // topIndex === 3 标识 符号 栈顶 为 ( // index > 0 示意 以后检索 符号 不为 ) // 因为符号 不为 ) 时,pop掉的 ( 须要加回去 if (topIndex === 3 && index > 0) { stack.push(topValue); break; } } if (index > 0) stack.push(str); } } else if (/\d/.test(str)) { output.push(str); }}
计算表达式
其实次要麻烦就在转换表达式,转换实现后就很简略了。后缀表达式的计算逻辑就是遇到操作出栈两个元素,进行运算。
- 申明计算方法
- 申明计算方法对应的操作符
const counterFn = { add: (a: number, b: number) => a + b, minus: (a: number, b: number) => a - b, multiply: (a: number, b: number) => a * b, divide: (a: number, b: number) => a / b,};const counterMap = { "+": counterFn.add, "-": counterFn.minus, "*": counterFn.multiply, x: counterFn.multiply, "/": counterFn.divide,};
遍历后缀表达式,数字之间入栈
遇到操作符,获取对应的计算方法,出栈两个元素,进行计算
export function counter(list: string[]) { const stack: number[] = []; let result = 0; for (let i = 0; i < list.length; i++) { const value = list[i]; if (/\d/.test(value)) { stack.push(Number(value)); } else { const a = stack.pop() || 0; const b = stack.pop() || 0; const res = counterMap[value](b, a); stack.push(res); result = res; } } return result;}
扩大
再顺便加一点好玩的货色,输出的是中文数字的话能计算吗
OK,来做一下中文数字的兼容,去写中文数字转换阿拉伯数字太麻烦也有点简单,我间接找了一个第三库cnwhy/nzh
其实应用库就变得很简略了,因为外围逻辑咱们曾经写好了,只须要在解决字符串的时候兼容中文数字就能够了。
给咱们的正则匹配项加上中文操作符,就能够依据 加|减|乘|除|左括号|右括号
来进行宰割。还有一个问题,那数字呢,数字还是中文啊,不能进行计算;当然也能够在这里遍历宰割的数组,用map把去解决中文数字,然而我不想在这里多一次数组的循环,去转换逻辑外面做。
const reg = new RegExp(/(\+|\*|x|\/|\(|\)|\^|加|减|乘|除|左括号|右括号)/g);
只须要在转换的判断逻辑最初加上一个else调用nzh提供的 Nzh.cn.decodeS
办法转换一下即可。因为除了操作符和数字,就是中文数字了。
import Nzh from "nzh";if (/\d/.test(str)) { output.push(str);} else { output.push(Nzh.cn.decodeS(str));}
仔细的小伙伴应该发现了,还有一个问题,转换了数字,操作符还是中文呢。中文操作符咱们不在这里解决,只须要再计算中给 counterMap
多加几个映射就能够了。
const counterMap: any = { "+": counterFn.add, "-": counterFn.minus, "*": counterFn.multiply, x: counterFn.multiply, "/": counterFn.divide, 加: counterFn.add, 减: counterFn.minus, 乘: counterFn.multiply, 除: counterFn.divide,};
OK,功败垂成,一个数学表达式的计算工具就实现了,并且反对中文数字计算。当然,还有一个问题,没有对输出的字符串做错误处理以及提醒,明天临时不弄了,改天再弄吧。