使用中缀表达式计算公式

273 阅读4分钟

前言

起因是因为公司有这么一个需求:

将下面数据带入公式

let data = { ActualHours: 0, AdjustmentHours: 12, TotalHours: 12,ChargeHours: 0}
let formulaArr = [
    {
        "BusinessCategory": "GDS",
        "CalculationFormulaOpe": "{TotalHours}*2.5",
        "CalculationFormulaSer": "{TotalHours}/1.2*670+{TotalHours}/1.2*0.2*1250",
        "CalculationFormulaTotalHours": "{ChargeHours}+{AdjustmentHours}"
    }
]

比如带入CalculationFormulaSer这个公式就变成了12/1.2*670+12/1.2*0.2*1250也就等于12

一、分析需求

如果分为大块则不难想到这里需要做两件事

  • 将data的数据带入公式
  • 计算带入后的公式

那么第一步带入公式不难想到使用正则替换,具体难在计算,可能你会觉得eval方法很不错,但是这个方法风险太高,然后我就想到了使用中缀表达式

二、 将数据带入公式

这里我在String上添加了一个format方法

String.prototype.format = function (args) {
	var result = this;
	if (arguments.length > 0) {
		if (arguments.length == 1 && typeof args === 'object') {
			for (var key in args) {
				if (args[key] != undefined) {
					var reg = new RegExp('({' + key + '})', 'g');
					result = result.replace(reg, args[key]);
				}
			}
		} else {
			for (var i = 0; i < arguments.length; i++) {
				if (arguments[i] != undefined) {
					var reg = new RegExp('({)' + i + '(})', 'g');
					result = result.replace(reg, arguments[i]);
				}
			}
		}
	}
	return result;
};

//使用 例如: 
console.log(formulaArr[0].CalculationFormulaSer.format(data))//'12/1.2*670+12/1.2*0.2*1250'

三、计算公式

首先就是使用eval例如eval('12/1.2*670+12/1.2*0.2*1250')

image.png

但是上面也说了这个方法不安全,那么我上面提到了中缀表达式,那么什么是中缀表达式,是不是还有前缀表达式和后缀表达式,别急,我接下来一一介绍

四、前缀表达式、中缀表达式、和后缀表达式

对此次不感兴趣的同学可以直接跳过

前缀表达式

前缀表达式(也被称为波兰表示法,Polish Notation)是一种数学表达式的表示方法,其中运算符位于其操作数之前。这种表示法具有不需要括号即可完全消除歧义的优点。前缀表达式是逆波兰表示法(后缀表达式,Reverse Polish Notation)的另一种形式。

例如,考虑一个简单的算术表达式:(2 + 3) * 4。在前缀表示法中,它可以表示为 * + 2 3 4。在这个例子中,我们首先看到乘法运算符 *,接下来是加法运算符 +,然后是操作数 23,最后是操作数 4。前缀表示法中的计算从左到右进行,先进行加法操作(2 + 3 = 5),然后进行乘法操作(5 * 4 = 20)。

前缀表达式在计算机科学领域中也有广泛的应用,特别是在解析和计算表达式的算法中。由于其无需括号和运算符优先级的特点,使得它在处理复杂数学表达式时更简单且易于理解。

后缀表达式

后缀表达式(也称为逆波兰表示法,Reverse Polish Notation,简称 RPN)是一种数学表达式的表示方法,其中运算符位于其操作数之后。后缀表达式不需要使用括号,因为运算符的顺序已经明确了操作数的组合方式和运算顺序,从而消除了歧义。

例如,考虑一个简单的算术表达式:(2 + 3) * 4。在后缀表示法中,它可以表示为 2 3 + 4 *。在这个例子中,我们首先看到操作数 23,接下来是加法运算符 +,然后是操作数 4,最后是乘法运算符 *。后缀表示法中的计算从左到右进行,先进行加法操作(2 + 3 = 5),然后进行乘法操作(5 * 4 = 20)。

后缀表达式在计算机科学领域有广泛的应用,尤其是在解析和计算表达式的算法中。由于其无需括号和运算符优先级的特点,使得它在处理复杂数学表达式时更简单且易于理解。后缀表达式通常可以通过堆栈(Stack)这种数据结构来实现高效的计算。

中缀表达式

中缀表达式(Infix Notation)是一种数学表达式的表示方法,其中运算符位于其两个操作数之间。这是我们在日常生活中最常见的表达式表示方式。在中缀表达式中,我们通常需要使用括号来消除歧义,以便正确地表示运算符的优先级。

例如,考虑一个简单的算术表达式:(2 + 3) * 4。这个表达式本身就是一个中缀表达式。在这个例子中,我们使用括号来表示加法运算需要先于乘法运算进行。在中缀表达式中,我们根据运算符的优先级和结合性规则来计算表达式的值。

相比前缀表达式(波兰表示法,Polish Notation)和后缀表达式(逆波兰表示法,Reverse Polish Notation),中缀表达式在计算过程中需要更多的注意力来处理括号和运算符优先级。然而,中缀表达式在日常生活中更常见,因为它更接近我们通常书写和阅读数学表达式的方式。在计算机科学领域,有很多算法可以将中缀表达式转换为前缀或后缀表达式,以便更简单地计算和解析这些表达式。

接下来说人话,以我的话总结下:

  1. 前缀表达式就是符号在前面
  2. 中缀表达式就是我们常见的
  3. 后缀表达式就是符号在后面

关于前缀表达式与后缀表达式的运算感兴趣的同学可以自己找找看

四、计算中缀表达式

首先也是拆解开要考虑的情况

  1. 定义运算符及其优先级和运算方法。operators 对象定义了四种基本的算术运算符(加、减、乘、除),包括它们的优先级(precedence)和执行运算的方法(apply)。

  2. 初始化输出队列 outputQueue 和运算符栈 operatorStack。这两个数据结构在算法过程中用于暂存操作数和运算符。

  3. 使用正则表达式将输入表达式拆分成数字、运算符和括号的数组。用tokens 数组存储了拆分后的表达式元素。

  4. 遍历拆分后的数组(tokens),处理每个元素。对于每个元素,根据其类型执行相应的操作:

    • 如果是数字,则解析并添加到输出队列中;
    • 如果是左括号,则压入运算符栈;
    • 如果是右括号,则处理左括号之前的运算符,并将结果压入输出队列;
    • 如果是运算符,则处理运算符栈,并将结果压入输出队列。
  5. 当运算符栈非空时,继续处理剩余的运算符,并将结果压入输出队列。

  6. 输出队列的第一个元素就是计算结果。 接下来直接上代码

/** 计算中缀表达式 */
function evaluateInfixExpression(expression) {
	// 定义运算符及其优先级和运算方法
	const operators = {
		'+': { precedence: 1, apply: (a, b) => a + b },
		'-': { precedence: 1, apply: (a, b) => a - b },
		'*': { precedence: 2, apply: (a, b) => a * b },
		'/': { precedence: 2, apply: (a, b) => a / b },
	};

	// 初始化输出队列和运算符栈
	const outputQueue = [];
	const operatorStack = [];

	// 使用正则表达式将输入表达式拆分成数字、运算符和括号的数组
	const tokens = expression.match(/\d+\.\d+|\d+|[+\-*/()]|\s/g);
	// 遍历拆分后的数组
	tokens.forEach((token) => {
		/**
		 * 注: 这里判断分4中情况 数字、左括号、右括号,算术运算符
		 */
		if (parseFloat(token)) {
			// 如果是数字,则解析并添加到输出队列中
			outputQueue.push(parseFloat(token));
		} else if (token === '(') {
			// 如果是左括号,则压入运算符栈
			operatorStack.push(token);
		} else if (token === ')') {
			// 如果是右括号,则处理左括号之前的运算符
			while (operatorStack[operatorStack.length - 1] !== '(') {
				// 弹出栈顶运算符
				const operator = operatorStack.pop();
				// 弹出输出队列的后两个元素作为操作数
				const b = outputQueue.pop();
				const a = outputQueue.pop();
				// 将操作数应用到弹出的运算符,并将结果压入输出队列
				outputQueue.push(operators[operator].apply(a, b));
			}
			// 弹出左括号
			operatorStack.pop();
		} else if (operators[token]) {
			// 如果是运算符,则处理运算符栈
			const currentOperator = operators[token];
			// 当运算符栈非空且栈顶不是左括号且当前运算符优先级小于或等于栈顶运算符优先级时,执行循环
			while (operatorStack.length && operatorStack[operatorStack.length - 1] !== '(' && operators[operatorStack[operatorStack.length - 1]].precedence >= currentOperator.precedence) {
				// 弹出栈顶运算符
				const operator = operatorStack.pop();
				// 弹出输出队列的后两个元素作为操作数
				const b = outputQueue.pop();
				const a = outputQueue.pop();
				// 将操作数应用到弹出的运算符,并将结果压入输出队列
				outputQueue.push(operators[operator].apply(a, b));
			}
			// 将当前运算符压入运算符栈
			operatorStack.push(token);
		}
	});

	// 当运算符栈非空时,继续处理剩余的运算符
	while (operatorStack.length) {
		// 弹出栈顶运算符
		const operator = operatorStack.pop();
		// 弹出输出队列的后两个元素作为操作数
		const b = outputQueue.pop();
		const a = outputQueue.pop();
		// 将操作数应用到弹出的运算符,并将结果压入输出队列
		outputQueue.push(operators[operator].apply(a, b));
	}

	// 输出队列的第一个元素就是计算结果
	return outputQueue[0];
}

evaluateInfixExpression('12/1.2*670+12/1.2*0.2*1250')//9200

最后希望这篇文章能帮到同样有这种困惑的同学