前言

做类似 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入门