使用 ANTLR 实现公式解析

前言

做类似 BI 的产品时,会遇到公式解析场景。用户(前端)输入一个公式字符串,后端要解析为一个具体的函数对象。

我们使用 ANTLR 来做公式解析。

关于 ANTLR 的基本用法,及它在项目中的基本配置,参见:ANTLR4入门

本文并不是手把手教程,只讲核心点,其他不赘述。

定义函数类

我们会有一个 Function 的抽象类/接口,其他所有函数都继承 Function。函数可以套函数。

根据实际业务需求来定义,这里不是本文重点,略。

我们要将公式字符串解析为 Function 对象。举个例子,对于表达式字符串“a+b+c”,可以表示为一个 ADD 函数,有三个节点(参数):a、b、c。

分析公式表达式

一个公式表达式中,可能出现以下元素。

值类型:
  1. 表格列
  2. 整数
  3. 浮点型数字
  4. 字符串
函数类型:
  1. 加减乘除算术运算
  2. 自定义的函数 比如:sum()、count()等
自定义的关键字:
  1. distinct (实际抽象为函数)

写语法和词法

Formula.g4

grammar Formula;

expr
   : expr '/' expr                                                 # Divide  // 除法
   | expr '*' expr                                                 # Multiply // 乘法
   | expr '-' expr                                                 # Subtract  // 减法
   | expr '+' expr                                                 # Add     //加法
   | FLOAT                                                       # Float   // 浮点型
   | INTEGER                                                  # Integer   //整数
   | '(-'expr')'                                               # Negative  // 负运算
   | '('expr')'                                                   # Parens  // 括号运算 
   | ABS'('expr')'                                                   # Abs  // 绝对值
   | MAX'('expr (','expr)*')'                                     # Max   // 最大值
   | MIN'('expr (','expr)*')'                                     # Min  // 最小值
   | SUM'('expr (','expr)*')'                                     # Sum  //求和
   | '${' MUL_IDENTIFIER '}'                                      # Column  // 表格列
;

# 词法
//函数名
ABS                            :[aA][bB][sS];                      //绝对值
MAX                            :[mM][aA][xX];                      //返回一组值中的最大值。MAX(number1, [number2], ...)
MIN                            :[mM][iI][nN];                      //返回一组值中的最小值。MIN(number1, [number2], ...)
SUM                            :[sS][uU][mM];                      //返回一组值的和。SUM(number1, [number2], ...)

INTEGER                      :[0-9]+;                           //整数,包含正整数、负整数、零
FLOAT                        :[0-9]* '.' INTEGER+;                 //浮点数,包含正浮点数,负浮点数、零(0.0)
STRING                                                            // 字符串类型 
    : '\'' ( ~('\''|'\\') | ('\\' .) )* '\''
    ;



WHITE_SPACE                   : [ \t\r]+ -> skip ;                  //空白定义,可以是空格、制表符,换行符


MUL_IDENTIFIER
    : IDENTIFIER '.'  IDENTIFIER
    ;

IDENTIFIER                                             //数字、字母以及下划线表示的标识符
    : (LETTER | DIGIT | '_')+
    ;
DIGIT                                         //数字
    : [0-9]
    ;

LETTER                                        //字母
    : [A-Z] | [a-z]
    ;

实现 Visitor

编译之后,我们来写 FormulaVisitorImpl。

public class FormulaVisitorImpl extends FormulaBaseVisitor<Function> {
    
    /**
    *  访问 ➕ 节点调用
    *  返回值为我们的 Function 类型 实现比较简单
    */
    @Override
    public Function visitAdd(FormulaParser.AddContext ctx) {
        AddFunction addFunction = new AddFunction();
        List<Function> argList = new ArrayList<>();
        argList.add(visit(ctx.expr(0)));
        argList.add(visit(ctx.expr(1)));
        addFunction.setArgList(argList);
        return addFunction;
    }

    /**
    *  访问整数节点调用
    */
    @Override
    public Function visitInteger(FormulaParser.IntegerContext ctx) {
        ConstantFunction constantFunction = new ConstantFunction();
        constantFunction.setValueType(ValueTypeEnum.NUMBER);
        constantFunction.setValue(Integer.valueOf(ctx.getText()));
        return constantFunction;
    }
}

继续写其他的 Visit 逻辑。

完成解析

ANTLRInputStream stream = new ANTLRInputStream("1 + 1");
FormulaLexer lexer = new FormulaLexer(stream);
CommonTokenStream tkn = new CommonTokenStream(lexer);
FormulaParser parser = new FormulaParser(tkn);
ParseTree tree = parser.expr();
FormulaVisitorImpl visitor = new FormulaVisitorImpl();
Function ans = visitor.visit(tree);
System.out.println(ans.toExpression());

本文比较简略,仅为思路整理。ANTLR 的细节用法(手把手教程),请看上一篇文章:ANTLR4入门

站长数十年成长感悟,输入暗号查看

桩白墨公众号上还能加入书舟书友群、美团外卖红包群,免费领取站桩和原创 chatgpt 教程哦
暗号:
请关注本站微信公众号,回复“暗号”,即可获取。在微信里搜索“桩白墨”或者“baimoz”或者微信扫描右侧二维码都可以关注站长微信公众号。