js实现数学表达式计算,并兼容中文数字计算

介绍

要实现数学表达式计算器,涉及到两个方法,波兰表示法(维基百科)逆波兰表示法(维基百科),详细的概念可以自行查看百科。

波兰表达式是将操作符前置,也就是前缀表达式;逆波兰表达式是将操作符后置,即后缀表达式。而我们正常去输入的叫中缀表达式,要是直接去对中缀表达式进行计算,那么优先级的判断就不是很方便,而前缀或者后缀就很方便去写计算逻辑。

效果展示

实现思路

转换表达式

所以首要任务是将输入的数学表达式进行转换,在这里我选择的是将中缀转换成后缀表达式来计算,当然用前缀也一样。

我们需要声明两个栈,一个用来存符号,一个用来存输出的后缀表达式。

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 有两种情况,数字或者是操作符:

数字很简单,直接 pushoutput (最终的表达式)中;

如果是操作符,就要分情况处理了。

  • 当栈顶没有元素时,直接 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,大功告成,一个数学表达式的计算工具就完成了,并且支持中文数字计算。当然,还有一个问题,没有对输入的字符串做错误处理以及提示,今天暂时不弄了,改天再弄吧。

你可能感兴趣的:(js实现数学表达式计算,并兼容中文数字计算)