共计 3255 个字符,预计需要花费 9 分钟才能阅读完成。
介绍
要实现数学表达式计算器,波及到两个办法,波兰表示法 (维基百科) 和逆波兰表示法(维基百科),具体的概念能够自行查看百科。
波兰表达式是将操作符前置,也就是前缀表达式;逆波兰表达式是将操作符后置,即后缀表达式。而咱们失常去输出的叫中断表达式,要是间接去对中断表达式进行计算,那么优先级的判断就不是很不便,而前缀或者后缀就很不便去写计算逻辑。
成果展现
- 在线 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,功败垂成,一个数学表达式的计算工具就实现了,并且反对中文数字计算。当然,还有一个问题,没有对输出的字符串做错误处理以及提醒,明天临时不弄了,改天再弄吧。