Java 实现数学公式的正则校验和结果计算

最近工作中遇到一个需求,需要校验数学公式字符串是否合法,并对公式进行计算,类似于一个简单计算器的效果。

公式

数学公式中有括号,运算符和变量,其中变量是从数据库中读取的,可以任意添加和删除。

假设内置变量为:height、length、width、num。对于公式字符串如 (length*(1+width)/height)*num,需要校验公式格式是否合法,然后对变量进行赋值,计算公式的运算结果。

1. 公式校验

对公式进行校验,除了一些常规性的检查之外,还需要对公式的格式进行优化,为后续的公式计算扫清障碍。

优化内容包括:

  • 去掉公式中的空格。
  • 对负数和正数进行补零,例如 -a 优化为 0-a
  • 补充两个括号之间的乘法运算符,例如 (a+b)(b+c) 优化为 (a+b)*(b+c)

完整的校验逻辑如下:

import org.apache.commons.lang3.StringUtils;
import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;

/**
 * @author Sumkor
 * @since 2021/7/14
 */
public class ValidateTest {

    @Test
    public void test() {
        List<String> variables = Arrays.asList("height", "length", "width", "num");

        String result01 = validate("(height+length)(width+num)", variables);
        System.out.println("result01 = " + result01);

        String result02 = validate("-num+100", variables);
        System.out.println("result02 = " + result02);

        String result03 = validate("(length*(1+width)/height)*num", variables);
        System.out.println("result03 = " + result03);
    }

    /**
     * 使用正则来校验数学公式
     *
     * @param expression 数学公式,包含变量
     * @param variables  内置变量集合
     */
    private String validate(String expression, List<String> variables) {
        if (variables == null || variables.isEmpty()) {
            throw new RuntimeException("内置变量为空");
        }
        // 去空格
        expression = expression.replaceAll(" ", "");
        // 连续运算符处理
        if (expression.split("[\\+\\-\\*\\/]{2,}").length > 1) {
            throw new RuntimeException("公式不合法,包含连续运算符");
        }
        if (StringUtils.contains(expression, "()")) {
            throw new RuntimeException("公式不合法,包含空括号");
        }
        expression = expression.replaceAll("\\)\\(", "\\)*\\(");
        expression = expression.replaceAll("\\(\\-", "\\(0-");
        expression = expression.replaceAll("\\(\\+", "\\(0+");
        // 校验变量
        String[] splits = expression.split("\\+|\\-|\\*|\\/|\\(|\\)");
        for (String split : splits) {
            if (StringUtils.isBlank(split) || Pattern.matches("-?(0|([1-9]\\d*))(\\.\\d+)?", split)) {
                continue;
            }
            if (!variables.contains(split)) {
                throw new RuntimeException("公式不合法,包含非法变量或字符");
            }
        }
        // 校验括号
        Character preChar = null;
        Stack<Character> stack = new Stack<>();
        String resultExpression = expression;
        for (int i = 0; i < expression.length(); i++) {
            char currChar = expression.charAt(i);
            if (i == 0) {
                if (Pattern.matches("\\*|\\/", String.valueOf(currChar))) {
                    throw new RuntimeException("公式不合法,以错误运算符开头");
                }
                if (currChar == '+') {
                    resultExpression = expression.substring(1);
                }
                if (currChar == '-') {
                    resultExpression = "0" + expression;
                }
            }
            if ('(' == currChar) {
                stack.push('(');
            } else if (')' == currChar) {
                if (stack.size() > 0) {
                    stack.pop();
                } else {
                    throw new RuntimeException("公式不合法,括号不配对");
                }
            }
            if (preChar != null && preChar == '(' && Pattern.matches("[\\+\\-\\*\\/]+", String.valueOf(currChar))) {
                throw new RuntimeException("公式不合法,左括号后是运算符");
            }
            if (preChar != null && preChar == ')' && !Pattern.matches("[\\+\\-\\*\\/]+", String.valueOf(currChar))) {
                throw new RuntimeException("公式不合法,右括号后面不是运算符");
            }
            if (i == expression.length() - 1) {
                if (Pattern.matches("\\+|\\-|\\*|\\/", String.valueOf(currChar)))
                    throw new RuntimeException("公式不合法,以运算符结尾");
            }
            preChar = currChar;
        }
        if (stack.size() > 0) {
            throw new RuntimeException("公式不合法,括号不配对");
        }
        return resultExpression;
    }
}

2. 公式计算

当数学公式字符串的合法性校验通过,并且对其中的变量进行赋值之后,就可以计算它的运算结果了。

这里分别使用 JDK 内置的 JS 引擎、本人实现的后缀表达式算法,来计算数学公式字符串的运算结果,并对比两者的运算效率。

2.1 使用 JS 引擎

利用 JDK 内置的 JS 引擎来计算结果,即方便,结果又准确。缺点是计算效率比较低。

@Test
public void scriptEngine() throws ScriptException {
	String str = "43*(2+1.4)+2*32/(3-2.1)";
	ScriptEngineManager manager = new ScriptEngineManager();
	ScriptEngine engine = manager.getEngineByName("js");
	Object result = engine.eval(str);
	System.out.println("结果类型:" + result.getClass().getName() + ",计算结果:" + result);
}

运行结果:

结果类型:java.lang.Double,计算结果:217.3111111111111

2.2 使用算法

自行编写一个算法来完成公式计算,可以在更短的时间内拿到计算结果,不过需要保证算法的准确性。

网上这篇文章 java实现计算复杂数学表达式 提供了一个很好的思路,不过这篇文章的代码只支持单位数的计算,并且在部分情况下会计算错误。

思路就是分两步来进行

  1. 翻译输入的数学表达式,也就是中缀表达式转后缀表达式。例如 a+b*(c-d) 转为后缀表达式就是 abcd-*+
  2. 对后缀表达式计算结果。这里用到了栈存储计算结果,每次都是对两个数计算,例如 abcd-*+,计算方法是先从头遍历,数字直接入栈,当遇到计算符,则从栈顶取出来两个数计算然后再把结果压栈,最终全部计算完之后栈里面只剩下一个元素就是结果。

本文同样以这种思路进行实现,支持对多位数、小数点的计算,并进行验证。

2.2.1 如何使用后缀表达式

本算法的核心思路是把数学公式从中缀表达式转后缀表达式,例如 3-10*2+5 转换为后缀表达式就是 3, 10, 2, *, -, 5, +
在讨论如何生成后缀表达式之前,先看下如何使用后缀表达式来计算结果。

参数定义

  • 使用链表 LinkedList 来存储后缀表达式的每一个元素,元素可能是数值,也可能是运算符,因此定义元素类型为字符串。
  • 由于支持多位数、小数点的计算,使用 BigDecimal 类型作为运算结果。
/**
 * 根据后缀表达式,得到计算结果
 */
private static BigDecimal doCalculate(LinkedList<String> postfixList) {
	// 操作数栈
	Stack<BigDecimal> numStack = new Stack<>();
	while (!postfixList.isEmpty()) {
		String item = postfixList.removeFirst();
		BigDecimal a, b;
		switch (item) {
			case "+":
				a = numStack.pop();
				b = numStack.pop();
				numStack.push(b.add(a));
				break;
			case "-":
				a = numStack.pop();
				b = numStack.pop();
				numStack.push(b.subtract(a));
				break;
			case "*":
				a = numStack.pop();
				b = numStack.pop();
				numStack.push(b.multiply(a));
				break;
			case "/":
				a = numStack.pop();
				b = numStack.pop();
				numStack.push(b.divide(a, 2, RoundingMode.HALF_UP));
				break;
			default:
				numStack.push(new BigDecimal(item));
				break;
		}
	}
	return numStack.pop();
}

2.2.2 如何生成后缀表达式

利用后缀表达式来获得计算结果,实现起来比较简单,关键在于如何转换得到正确的后缀表达式。

参数定义

  • 定义一个运算符栈 Stack<Character> optStack,用于按优先级来调整运算符的顺序。
  • 定义一个多位数链 LinkedList<Character> multiDigitList,用于暂存数学算式中的多位数、小数点。
  • 出参使用链表 LinkedList<String> postfixList 来存储后缀表达式。

逐个字符遍历数学算式的字符串,生成后缀表达式,步骤如下:

  1. 如果遇到数字或小数点,则将当前字符暂存到多位数链中。
  2. 如果遇到运算符,先将多位数链中的已有内容全部取出,存入后缀表达式链,再进行下一步判断。
  3. 如果当前字符是左括号,直接将其压入运算符栈。
  4. 如果当前字符是加减乘除运算符,则需要与运算符栈栈顶的运算符进行对比:
    如果当前运算符优先级更高,则直接入栈;
    否则反复弹出栈顶元素至后缀表达式链,直到当前运算符优先级高于栈顶运算符,再将当前运算符入栈。
  5. 如果当前字符是右括号,反复将运算符栈顶元素弹出到后缀表达式,直到栈顶元素是左括号为止,并将左括号从栈中弹出丢弃。
  6. 遍历结束时,如果多位数链或运算符栈中还有数据,则补到后缀表达式链中。

实现如下:

/**
 * 将中缀表达式,转换为后缀表达式,支持多位数、小数
 * 
 * @author Sumkor
 * @since 2021/7/14
 */
private static LinkedList<String> getPostfix(String mathStr) {
	// 后缀表达式链
	LinkedList<String> postfixList = new LinkedList<>();
	// 运算符栈
	Stack<Character> optStack = new Stack<>();
	// 多位数链
	LinkedList<Character> multiDigitList = new LinkedList<>();
	char[] arr = mathStr.toCharArray();
	for (char c : arr) {
		if (Character.isDigit(c) || '.' == c) {
			multiDigitList.addLast(c);
		} else {
			// 处理当前的运算符之前,先处理多位数链中暂存的数据
			if (!multiDigitList.isEmpty()) {
				StringBuilder temp = new StringBuilder();
				while (!multiDigitList.isEmpty()) {
					temp.append(multiDigitList.removeFirst());
				}
				postfixList.addLast(temp.toString());
			}
		}
		// 如果当前字符是左括号,将其压入运算符栈
		if ('(' == c) {
			optStack.push(c);
		}
		// 如果当前字符为运算符
		else if ('+' == c || '-' == c || '*' == c || '/' == c) {
			while (!optStack.isEmpty()) {
				char stackTop = optStack.pop();
				// 若当前运算符的优先级高于栈顶元素,则一起入栈
				if (compare(c, stackTop)) {
					optStack.push(stackTop);
					break;
				}
				// 否则,弹出栈顶运算符到后缀表达式,继续下一次循环
				else {
					postfixList.addLast(String.valueOf(stackTop));
				}
			}
			optStack.push(c);
		}
		// 如果当前字符是右括号,反复将运算符栈顶元素弹出到后缀表达式,直到栈顶元素是左括号(为止,并将左括号从栈中弹出丢弃。
		else if (c == ')') {
			while (!optStack.isEmpty()) {
				char stackTop = optStack.pop();
				if (stackTop != '(') {
					postfixList.addLast(String.valueOf(stackTop));
				} else {
					break;
				}
			}
		}
	}
	// 遍历结束时,若多位数链中具有数据,说明公式是以数字结尾
	if (!multiDigitList.isEmpty()) {
		StringBuilder temp = new StringBuilder();
		while (!multiDigitList.isEmpty()) {
			temp.append(multiDigitList.removeFirst());
		}
		postfixList.addLast(temp.toString());
	}
	// 遍历结束时,运算符栈若有数据,说明是由括号所致,需要补回去
	while (!optStack.isEmpty()) {
		postfixList.addLast(String.valueOf(optStack.pop()));
	}
	return postfixList;
}

对于运算符栈,左括号会直接入栈,右括号不会入栈。
因此,对比栈顶元素与当前元素的优先级,代码如下:

/**
 * 比较优先级
 * 返回 true 表示 curr 优先级大于 stackTop
 */
private static boolean compare(char curr, char stackTop) {
	// 左括号会直接入栈,这里是其他运算符与栈顶左括号对比
	if (stackTop == '(') {
		return true;
	}
	// 乘除法的优先级大于加减法
	if (curr == '*' || curr == '/') {
		return stackTop == '+' || stackTop == '-';
	}
	// 运算符优先级相同时,先入栈的优先级更高
	return false;
}

2.2.3 验证

编写多个 test case 如下。

@Test
public void calculateTest() {
	String str = "5-1*(5+6)+2";
	System.out.println(str + " = " + calculate(str));

	str = "50-1*(5+6)+2";
	System.out.println(str + " = " + calculate(str));

	str = "(50.5-1)*(5+6)+2";
	System.out.println(str + " = " + calculate(str));

	str = "1+2*(3-4*(5+6))";
	System.out.println(str + " = " + calculate(str));

	str = "1/2*(3-4*(5+6))*10";
	System.out.println(str + " = " + calculate(str));

	str = "43*(2+1)+2*32+98";
	System.out.println(str + " = " + calculate(str));

	str = "3-10*2+5";
	System.out.println(str + " = " + calculate(str));
}

/**
 * 1. 将中缀表达式转后缀表达式
 * 2. 根据后缀表达式进行计算
 */
public static BigDecimal calculate(String mathStr) {
	if (mathStr == null || mathStr.length() == 0) {
		return null;
	}
	LinkedList<String> postfixList = getPostfix(mathStr);
	// System.out.println("后缀表达式:" + postfixList.toString());
	return doCalculate(postfixList);
}

执行结果如下,运算结果正确。

5-1*(5+6)+2 = -4
50-1*(5+6)+2 = 41
(50.5-1)*(5+6)+2 = 546.5
1+2*(3-4*(5+6)) = -81
1/2*(3-4*(5+6))*10 = -205
43*(2+1)+2*32+98 = 291
3-10*2+5 = -12

2.3 性能对比

执行 10000 条公式计算,对比耗时。

/**
 * 耗时对比
 */
@Test
public void vs() throws ScriptException {
	long start = System.currentTimeMillis();
	ScriptEngineManager manager = new ScriptEngineManager();
	ScriptEngine engine = manager.getEngineByName("js");
	for (int i = 0; i < 10000; i++) {
		String str = "43*(2+1.4)+2*32/(3-2.1)" + "+" + i;
		Object result = engine.eval(str);
	}
	System.out.println("耗时:" + (System.currentTimeMillis() - start));

	start = System.currentTimeMillis();
	for (int i = 0; i < 10000; i++) {
		String str = "43*(2+1.4)+2*32/(3-2.1)" + "+" + i;
		BigDecimal result = TransferTest.calculate(str);
	}
	System.out.println("耗时:" + (System.currentTimeMillis() - start));
}

可以看到,JDK 内置 JS 引擎的计算效率,远不如后缀表达式算法。

耗时:5989
耗时:71

作者:Sumkor
链接:https://blog.csdn.net/weixin_52801742/article/details/118736283

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
A:下面是Java语言实现IP地址正则校验的示例代码: ``` import java.util.regex.Matcher; import java.util.regex.Pattern; public class IPValidator { private static final String IP_PATTERN = "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$"; private static final Pattern pattern = Pattern.compile(IP_PATTERN); public static boolean validateIP(String ip) { if (ip == null || ip.isEmpty()) { return false; } Matcher matcher = pattern.matcher(ip); return matcher.matches(); } public static void main(String[] args) { String ip1 = "192.168.0.1"; String ip2 = "10.0.0.256"; String ip3 = "abc.def.ghi.jkl"; System.out.println(ip1 + " is valid? " + validateIP(ip1)); System.out.println(ip2 + " is valid? " + validateIP(ip2)); System.out.println(ip3 + " is valid? " + validateIP(ip3)); } } ``` 在代码中,我们使用了正则表达式 `^([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])$` 对IP地址进行了校验。该正则表达式可以匹配符合IPv4标准的IP地址。然后,我们使用`Pattern.matcher()`方法将输入的IP地址与正则表达式进行匹配并返回验证结果。最后,在`main`方法中我们对三个不同的IP地址进行验证。 输出结果如下所示: ``` 192.168.0.1 is valid? true 10.0.0.256 is valid? false abc.def.ghi.jkl is valid? false ``` 示例代码中的正则表达式可用于校验符合IPv4标准的IP地址,但不包括IPv6地址。如果您需要校验IPv6地址,需要编写相应的正则表达式或使用IPv6库进行验证。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值