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

标签 java algorithm parsing recursive-descent

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

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,

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

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

最佳答案

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

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

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

token 类本身:

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 token 。最后我们添加一个EOF表示输入结束的标记。结果返回为特殊 TokenStream目的。这是 TokenStream将帮助我们进行解析的类:

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);
}

它是唯一用于评估给定数据集的表达式并在数据集匹配时返回 true 的方法。

最基本的表达式是 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类是 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这是一组 SubExprAND 连接的表达式运算符(operator):

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这是一组 AndExprOR 加入(通常 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

关于java - 解析逻辑运算 - AND、OR、动态循环条件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32193036/

相关文章:

java - 使用标记解析表达式

Java : Rounding a decimal value to HALF_EVEN

java - wsgen 引发 NoClassDefFoundError 异常

java - 我怎样才能确保 Spring 一次构造一个 bean

algorithm - 取三种可能情况中的最大值的最长公共(public)子序列

php - 递归算法似乎正确但不起作用

python - HackerRank 上的 SherlockAndAnagrams 算法解释错了吗?

java - 替代 Jackson @JsonSubTypes

string - 如何在 Perl 中将人的全名解析为用户名?

java动态选择要执行的类