【Java教程】数学表达式可以通过数学解析库来进行解析

零 Java教程评论54字数 4419阅读14分43秒阅读模式
【Java教程】数学表达式可以通过数学解析库来进行解析

前言

今天刚好遇到一个需求,用户输入任意公式,返回计算结果。

例子:文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/15863.html

properties

复制代码
工资 = "出勤天数 * 基本工资/当月工作日 + 绩效奖金 - 迟到早退扣钱"

这里分享一个解析数学表达式的方法,我这里使用的是递归下降解析器(recursive descent parser)的解析方法,这是一种自顶向下的解析方法。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/15863.html

EBNF

首先介绍一下EBNF(扩展巴科斯范式 , Extended Backus–Naur Form) , 它是一种表达形式语言文法的代码。有点类似于正则表达式文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/15863.html

简单举几个例子文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/15863.html

properties

复制代码
# 表示字母 
letter = 'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z'|'A'|'B'|'C'|'D'|'E'|'F'|'G'|'H'|'I'|'J'|'K'|'L'|'M'|'N'|'O'|'P'|'Q'|'R'|'S'|'T'|'U'|'V'|'W'|'X'|'Y'|'Z';
# 等同于letter = 'a' ... 'z' | 'A' ... 'Z' ;

# 表示数字 1-9
nonzero_digit = '1'...'9' ;

# 表示数字 0-9
digit =  '0' | nonzero_digit ;
# 表示单词
word = (letter | '_') { '_' | letter | digit } ;
# 表示整数
int = '0' | ['+'|'-'] nonzero_digit { digit } ;
  • | 表示或
  • ... 表示范围,可用于表示连续的字符
  • {} 花括号表示出现0次或多次, 类似正则的*
  • '' 表示字符串
  • [] 表示可选,类似正则的?

基本运算公式的EBNF

通过上面的例子,大家应该大概明白ebnf的机制,这里我们写一下包含+-*/运算表达式的ebnf文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/15863.html

properties

复制代码
digit = '0'...'9' ;
nonzero_digit = '1'...'9';
factor = nonzero_digit ['.' {digit} ] ;  #这里表示小数
term = factor { ('*'|'/') factor } ;
expression = term { ('+'|'-') term } ;

这里大家应该可以发现,优先级越高的运算符需要优先计算;+-优先级最低,因此放在最外层的expression中。在日常生活的口算中,我们应该也会把term中的*/优先计算,再做+-运算文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/15863.html

如:文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/15863.html

properties

复制代码
res = 1 + 2 * 3 + 4 * 5     
    =  1 + 6 + 20

# 这里的 1,6,20 相当于term
# 2,3,4 ,5 相当于factor


代码实现

版本一

到这里我们得出了运算表达式的ebnf,接下来我们来实现一个简单的解析器文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/15863.html

java

复制代码
public class SimpleExpressionParser {

    SimpleExpressionParser() {
    }

    int ch, pos = -1;
    String str;

    void nextChar() {
        ch = (++pos < str.length()) ? str.charAt(pos) : -1;
    }

    boolean expect(char expectChar) {
        while (ch == ' ') nextChar();
        if (ch == expectChar) {
            nextChar();
            return true;
        }
        return false;
    }

    public static BigDecimal parse(String str) {
        SimpleExpressionParser parser = new SimpleExpressionParser();
        parser.str = str;
        parser.nextChar();
        return parser.parseExpression();
    }

    /**
     * expression = term { ('+'|'-') term } ;
     */
    BigDecimal parseExpression() {
        BigDecimal x = parseTerm();
        for (; ; ) {
            if (expect('+')) x = x.add(parseTerm());
            else if (expect('-')) x = x.subtract(parseTerm());
            else return x;
        }
    }

    /**
     * term = factor { ('*'|'/') factor } ;
     */
    BigDecimal parseTerm() {
        BigDecimal x = parseFactor();
        for (; ; ) {
            if (expect('*')) x = x.multiply(parseFactor());
            else if (expect('/')) x = x.divide(parseFactor(), 4, BigDecimal.ROUND_HALF_UP); //保留4位小数
            else return x;
        }
    }

    /**
     * factor = nonzero_digit ['.' {digit} ] ;
     */
    BigDecimal parseFactor() {
        BigDecimal x;
        // 跳过空格
        while (ch == ' ') nextChar();

        int startPos = this.pos;
        if ('0' <= ch && ch <= '9' || ch == '.') {
            while ('0' <= ch && ch <= '9' || ch == '.') nextChar();
            x = new BigDecimal(str.substring(startPos, this.pos));
        } else {
            throw new RuntimeException("Unexpected: " + (char) ch);
        }
        return x;
    }

    public static void main(String[] args) {
        System.out.println(SimpleExpressionParser.parse(" 1 + 2*3-4.5/5"));
    }
}

结合ebnf范式去实现代码,我们应该可以很清晰的看到每一步要做什么。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/15863.html

版本二

上面的代码有些问题,比如没有处理括号,没有处理负数,这些问题,这里我们来改进一下文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/15863.html

ebnf范式

先按自己的理解写下ebnf范式

properties

复制代码
digit = '0'...'9' ;
letter = 'a'...'z' | 'A'...'Z';
nonzero_digit = '1'...'9';
variable = (letter | '_') { '_' | letter | digit } ; #这里表示变量
num = nonzero_digit ['.' {digit} ]

factor = ['+'|'-'] ( variable | num | '(' expression ')' ) ;
term = factor { ('*'|'/') factor } ;
expression = term { ('+'|'-') term } ;

代码实现

鉴于我们只改了factor的范式,因此我们只需要改一下parseFactor()方法即可

代码如下

java

复制代码
public class SimpleExpressionParser1 extends SimpleExpressionParser {

    Map<String, BigDecimal> map;

    public static BigDecimal parse(String str, Map<String, BigDecimal> map) {
        SimpleExpressionParser1 parser = new SimpleExpressionParser1();
        parser.str = str;
        parser.map = map;
        parser.nextChar();
        return parser.parseExpression();
    }

    private static boolean isLetter(int ch) {
        return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z';
    }

    private static boolean isNum(int ch) {
        return '0' <= ch && ch <= '9';
    }

    /**
     * factor = ['+'|'-'] ( variable | num | '(' expression ')' ) ;
     */
    BigDecimal parseFactor() {
        if (expect('+')) return parseFactor();
        if (expect('-')) return parseFactor().negate();

        BigDecimal x;
        int startPos = this.pos;
        if (expect('(')) {
            x = parseExpression();
            if (!expect(')')) throw new RuntimeException("Expected ')'");
        } else if (isNum(ch) || ch == '.') { //解析数字
            while (isNum(ch) || ch == '.') nextChar();
            x = new BigDecimal(str.substring(startPos, this.pos));
        } else if (isLetter(ch) || ch == '_') {  // 解析变量
            while (isLetter(ch) || isNum(ch) || ch == '_') nextChar();
            String var = str.substring(startPos, this.pos);
            x = map.get(var);
            if (x == null) {
                throw new RuntimeException("Unknown variable: " + var);
            }
        } else {
            throw new RuntimeException("Unexpected: " + (char) ch);
        }
        return x;
    }

    public static void main(String[] args) {
        // 预期值, 5
        System.out.println(SimpleExpressionParser1.parse("-a * ( a * -(-5+10*a))", new HashMap<>() {{
            put("a", new BigDecimal(1));
        }}));
        // 预期值,11
        System.out.println(SimpleExpressionParser1.parse("a * -b + c - (-d) * 3", new HashMap<>() {{
            put("a", new BigDecimal(1));
            put("b", new BigDecimal(2));
            put("c", new BigDecimal(3));
            put("d", new BigDecimal(-4));
        }}));
    }
}

到目前为止我们实现了一个简单的表达式解析器。有兴趣可以再自行实现一下 位运算(>>,<<),逻辑运算(&&,||,!),三目运算符(?:),函数调用(如求和,求最大/小值,开方),关系运算符(>,<,=)等。

可以尝试实现解析: a > b ? max(1,2,3,4*(5+3)%2,min(7,sqrt(5))) : 20

零
  • 转载请务必保留本文链接:https://www.0s52.com/bcjc/javajc/15863.html
    本社区资源仅供用于学习和交流,请勿用于商业用途
    未经允许不得进行转载/复制/分享

发表评论