解析逻辑运算 - AND、OR、动态循环条件

resolving logical operations - AND, OR, looping conditions dynamically

我有一个传入记录过滤器,其中存储了如下给出的逻辑子句。

Acct1 = 'Y' AND Acct2 = 'N' AND Acct3 = 'N' AND Acct4 = 'N' AND Acct5 = 'N' AND ((Acct6 = 'N' OR Acct7 = 'N' AND Acct1 = 'Y') AND Formatted= 'N' AND Acct9 = 'N' AND (Acct10 = 'N' AND Acct11 = 'N') AND EditableField= 'N' )

我输入到此子句的数据将来自 Csv 文件,如下所示。

Country,Type,Usage,Acct1,Acct2,Acct3,Acct4,Acct5,Acct6,Acct7,Formatted,Acct9,Acct10,Acct11,EditableField
USA,Premium,Corporate,Y,N,Y,N,N,N,Y,N,Y,N,Y,N,
Mexico,Premium,Corporate,Y,N,Y,N,Y,N,Y,N,Y,N,Y,N,
USA,Premium,Corporate,Y,N,Y,N,N,N,N,Y,Y,N,Y,N,
USA,Premium,Corporate,Y,N,Y,N,Y,N,Y,Y,Y,N,Y,N,

我将不得不根据子句中定义的条件过滤掉文件中的记录。这是一个简单子句的示例,但会有比这更多的内部条件,用户可以随时更改子句,记录必须依次通过 10 个这样的子句。

所以我正在寻找一种方法来动态解释该子句并将其应用于传入记录。请提供有关如何设计的建议/任何示例(如果可用)。

您所拥有的是一个用某种语言编写的表达式,它似乎符合 SQL 的 WHERE 子句的语法。所以你需要:

  • 此语言的解析器,可以构建 AST,然后是表达式树
  • 根据您的上下文评估表达式树的引擎(即根据名称 Acct1、Acct2 等解析的环境)

这是一种简单的语言,因此您可以手动构建解析器,或者查看 ANTLR 或 JavaCC - 在这种情况下,我建议您查看一些示例 (ANTLR or JavaCC) - 当然,您不需要完整的 SQL 解析器!只需提取您需要的位即可。

一种更简单的方法是用某种语言编写过滤器表达式,可以通过 Java 脚本接口调用,例如 Javascript 或 Groovy(或 Ruby, Python...)。我不建议在输入文本上使用 运行 find/replace 将 SQL 类语言转换为目标语言(例如 Python 有 andor 运算符 - 小写),因为根据输入字符串的内容,这很容易中断。

这是完整的解决方案,不包括第三方库,如 ANTLR 或 JavaCC。请注意,虽然它是可扩展的,但它的功能仍然有限。如果你想创建更复杂的表达式,你最好使用语法生成器。

首先,让我们编写一个分词器,将输入字符串拆分为分词。以下是令牌类型:

private static enum TokenType {
    WHITESPACE, AND, OR, EQUALS, LEFT_PAREN, RIGHT_PAREN, IDENTIFIER, LITERAL, EOF
}

令牌class本身:

private static class Token {
    final TokenType type;
    final int start; // start position in input (for error reporting)
    final String data; // payload

    public Token(TokenType type, int start, String data) {
        this.type = type;
        this.start = start;
        this.data = data;
    }

    @Override
    public String toString() {
        return type + "[" + data + "]";
    }
}

为了简化标记化,让我们创建一个正则表达式,它从输入字符串中读取下一个标记:

private static final Pattern TOKENS = 
        Pattern.compile("(\s+)|(AND)|(OR)|(=)|(\()|(\))|(\w+)|\'([^\']+)\'");

请注意,它有很多组,每个 TokenType 一个组,顺序相同(第一个是 WHITESPACE,然后是 AND,依此类推)。最后是分词器方法:

private static TokenStream tokenize(String input) throws ParseException {
    Matcher matcher = TOKENS.matcher(input);
    List<Token> tokens = new ArrayList<>();
    int offset = 0;
    TokenType[] types = TokenType.values();
    while (offset != input.length()) {
        if (!matcher.find() || matcher.start() != offset) {
            throw new ParseException("Unexpected token at " + offset, offset);
        }
        for (int i = 0; i < types.length; i++) {
            if (matcher.group(i + 1) != null) {
                if (types[i] != TokenType.WHITESPACE)
                    tokens.add(new Token(types[i], offset, matcher.group(i + 1)));
                break;
            }
        }
        offset = matcher.end();
    }
    tokens.add(new Token(TokenType.EOF, input.length(), ""));
    return new TokenStream(tokens);
}

我正在使用 java.text.ParseException。在这里,我们应用正则表达式 Matcher 直到输入结束。如果它在当前位置不匹配,我们将抛出异常。否则,我们寻找找到的匹配组并从中创建一个标记,忽略 WHITESPACE 标记。最后,我们添加一个 EOF 标记,表示输入结束。结果被 return 编辑为特殊的 TokenStream 对象。这是 TokenStream class 这将帮助我们进行解析:

private static class TokenStream {
    final List<Token> tokens;
    int offset = 0;

    public TokenStream(List<Token> tokens) {
        this.tokens = tokens;
    }

    // consume next token of given type (throw exception if type differs)
    public Token consume(TokenType type) throws ParseException {
        Token token = tokens.get(offset++);
        if (token.type != type) {
            throw new ParseException("Unexpected token at " + token.start
                    + ": " + token + " (was looking for " + type + ")",
                    token.start);
        }
        return token;
    }

    // consume token of given type (return null and don't advance if type differs)
    public Token consumeIf(TokenType type) {
        Token token = tokens.get(offset);
        if (token.type == type) {
            offset++;
            return token;
        }
        return null;
    }

    @Override
    public String toString() {
        return tokens.toString();
    }
}

所以我们有了分词器,太棒了。您现在可以使用 System.out.println(tokenize("Acct1 = 'Y' AND (Acct2 = 'N' OR Acct3 = 'N')"));

对其进行测试

现在让我们编写解析器,它将创建表达式的树状表示。首先是所有树节点的接口Expr

public interface Expr {
    public boolean evaluate(Map<String, String> data);
}

它是用于评估给定数据集的表达式的唯一方法,return如果数据集匹配则为真。

最基本的表达式是 EqualsExpr,类似于 Acct1 = 'Y''Y' = Acct1:

private static class EqualsExpr implements Expr {
    private final String identifier, literal;

    public EqualsExpr(TokenStream stream) throws ParseException {
        Token token = stream.consumeIf(TokenType.IDENTIFIER);
        if(token != null) {
            this.identifier = token.data;
            stream.consume(TokenType.EQUALS);
            this.literal = stream.consume(TokenType.LITERAL).data;
        } else {
            this.literal = stream.consume(TokenType.LITERAL).data;
            stream.consume(TokenType.EQUALS);
            this.identifier = stream.consume(TokenType.IDENTIFIER).data;
        }
    }

    @Override
    public String toString() {
        return identifier+"='"+literal+"'";
    }

    @Override
    public boolean evaluate(Map<String, String> data) {
        return literal.equals(data.get(identifier));
    }
}

toString()方法仅供参考,您可以删除它。

接下来我们将定义 SubExpr class 是 EqualsExpr 或更复杂的括号(如果我们看到括号):

private static class SubExpr implements Expr {
    private final Expr child;

    public SubExpr(TokenStream stream) throws ParseException {
        if(stream.consumeIf(TokenType.LEFT_PAREN) != null) {
            child = new OrExpr(stream);
            stream.consume(TokenType.RIGHT_PAREN);
        } else {
            child = new EqualsExpr(stream);
        }
    }

    @Override
    public String toString() {
        return "("+child+")";
    }

    @Override
    public boolean evaluate(Map<String, String> data) {
        return child.evaluate(data);
    }
}

接下来是 AndExpr,它是由 AND 运算符连接的一组 SubExpr 表达式:

private static class AndExpr implements Expr {
    private final List<Expr> children = new ArrayList<>();

    public AndExpr(TokenStream stream) throws ParseException {
        do {
            children.add(new SubExpr(stream));
        } while(stream.consumeIf(TokenType.AND) != null);
    }

    @Override
    public String toString() {
        return children.stream().map(Object::toString).collect(Collectors.joining(" AND "));
    }

    @Override
    public boolean evaluate(Map<String, String> data) {
        for(Expr child : children) {
            if(!child.evaluate(data))
                return false;
        }
        return true;
    }
}

为了简洁起见,我在 toString 中使用 Java-8 Stream API。如果你不能使用Java-8,你可以用for循环重写它或者完全删除toString

最后我们定义OrExpr,它是由OR加入的AndExpr的集合(通常OR的优先级低于AND)。它与 AndExpr:

非常相似
private static class OrExpr implements Expr {
    private final List<Expr> children = new ArrayList<>();

    public OrExpr(TokenStream stream) throws ParseException {
        do {
            children.add(new AndExpr(stream));
        } while(stream.consumeIf(TokenType.OR) != null);
    }

    @Override
    public String toString() {
        return children.stream().map(Object::toString).collect(Collectors.joining(" OR "));
    }

    @Override
    public boolean evaluate(Map<String, String> data) {
        for(Expr child : children) {
            if(child.evaluate(data))
                return true;
        }
        return false;
    }
}

最后的 parse 方法:

public static Expr parse(TokenStream stream) throws ParseException {
    OrExpr expr = new OrExpr(stream);
    stream.consume(TokenType.EOF); // ensure that we parsed the whole input
    return expr;
}

因此您可以解析表达式以获取 Expr 对象,然后根据 CSV 文件的行对其进行评估。我假设您能够将 CSV 行解析为 Map<String, String>。这是用法示例:

Map<String, String> data = new HashMap<>();
data.put("Acct1", "Y");
data.put("Acct2", "N");
data.put("Acct3", "Y");
data.put("Acct4", "N");

Expr expr = parse(tokenize("Acct1 = 'Y' AND (Acct2 = 'Y' OR Acct3 = 'Y')"));
System.out.println(expr.evaluate(data)); // true
expr = parse(tokenize("Acct1 = 'N' OR 'Y' = Acct2 AND Acct3 = 'Y'"));
System.out.println(expr.evaluate(data)); // false

提示:

一种可能的解决方案是将您的布尔条件值存储在单个字符串属性中,例如 "YNYNNNYNYNYN",或者,更好的是,打包为二进制整数。然后,对于给定的子句,生成所有接受的字符串的 table。联接操作将 return 所有需要的记录。

在生成 table.

时,您甚至可以通过将子句编号连接到接受的字符串来一次处理多个子句。

尽管 table 的大小在条件数量上可能呈指数级增长,但对于中等数量的条件来说,这仍然是易于管理的。

我不知道这在 Java 中的效率如何,但基本 string-replace 操作可能是一个简单的解决方案。

您从查询字符串开始:

Acct1 = 'Y' AND Acct2 = 'N' AND Acct3 = 'Y' AND Acct4 = 'N' AND Acct5 = 'N' OR ((Acct6 = 'N' OR Acct7 = 'N') AND Acct8 = 'N' AND Acct9 = 'Y' AND (Acct10 = 'N' OR Acct11 = 'N') AND Acct12 = 'N')

对于 csv 中的每一行,例如Y,N,Y,N,Y,N,Y,N,Y,N,Y,N string-replace 查询中的列 headers 按值;这给你:

Y = 'Y' AND N = 'N' AND Y = 'Y' AND N = 'N' AND Y = 'N' OR ((N = 'N' OR Y = 'N') AND N = 'N' AND Y = 'Y' AND (N = 'N' OR Y = 'N') AND N = 'N')

然后用布尔值替换比较:
- 将 N = 'N'Y = 'Y' 替换为 Y
- 将 N = 'Y'Y = 'N' 替换为 N

这将导致:

Y AND Y AND Y AND Y AND N OR ((Y OR N) AND Y AND Y AND (Y OR N) AND Y)

然后循环执行一些 string-replace 操作,这些操作用 Y 替换真值,用 N 替换假值:
- 将 Y AND Y 替换为 Y
- 将 N AND NN AND YY AND N 替换为 N
- 将 Y OR YN OR YY OR N 替换为 Y
- 将 N OR N 替换为 N
- 将 (N) 替换为 N
- 将 (Y) 替换为 Y

这将逐渐减少布尔语句:

Y AND Y AND Y AND Y AND N OR ((Y OR N) AND Y AND Y AND (Y OR N) AND Y)
Y AND Y AND N OR ((Y) AND Y AND (Y) AND Y)
Y AND N OR ( Y AND Y AND Y AND Y)
N OR ( Y AND Y )
N OR ( Y )
Y

如果查询包含不带方括号的隐式优先级,例如 N AND N OR Y AND Y,您希望 AND 优先于 OR,请始终用尽替换 AND 和替换前的括号 OR:

while (string length decreases) {
    while (string length decreases) {
        replace every "(Z)" by "Z"
        replace every "X AND Y" by "Z"
    }
    replace one "X OR Y" by "Z"
}

在这个缩减过程中,确保在每次迭代后检查字符串长度是否减少,以避免格式错误的查询导致无限循环。