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

2023-09-11 05:38:24 作者:共渡沧桑

我有存储与逻辑子句来电记录过滤器如下面给出的。

  Acct1 ='Y'和Acct2 ='N'和Acct3 ='N'和Acct4 ='N'和Acct5 ='N'AND((Acct6 ='N'OR Acct7 ='N'和Acct1 =Y),并格式化='N'和Acct9 ='N'AND(Acct10 ='N'和Acct11 ='N')和EditableField ='N')
 

我的数据输入到该条款​​将从CSV文件如下图所示。

Country,Type,Usage,Acct1,Acct2,Acct3,Acct4,Acct5,Acct6,Acct7,Formatted,Acct9,Acct10,Acct11,EditableField 美国,premium,企业,Y,N,Y,N,N,N,Y,N,Y,N,Y,N, 墨西哥,premium,企业,Y,N,Y,N,Y,N,Y,N,Y,N,Y,N, 美国,premium,企业,Y,N,Y,N,N,N,N,Y,Y,N,Y,N, 美国,premium,企业,Y,N,Y,N,Y,N,Y,Y,Y,N,Y,N,

我将必须基于该部分中限定的条件下在该文件中筛选出的记录。这是一个简单的条款的例子,但会有比这更内在的条件,只要用户想要的条款是可以改变的,并会有10个这样的条款的记录,必须通过顺序。

所以我在寻找一种方式来动态地跨preT的条款,并将其应用于对进入的记录。请给我提供你如何设计/例如任何建议,如果有的话。

解决方案 基本逻辑运算和逻辑门电路

下面是完整的解决方案,其中不包括像ANTLR的或的JavaCC第三方库。请注意,虽然它的可扩展性,它的能力仍然有限。如果你想创建更复杂的EX pressions,你最好使用语法生成。

首先,让我们写的标记生成这样会将输入字符串的标记。这里的令牌类型:

 私有静态枚举TokenType {
    WHITESPACE,AND,OR,等于LEFT_PAREN,RIGHT_PAREN,标识符,字面EOF
}
 

令牌类本身:

 私有静态类令牌{
    最后TokenType类型;
    最终诠释启动; //开始输入位置(错误报告)
    最终的字符串数据; //有效载荷

    公共令牌(TokenType型,诠释开始,字符串数据){
        this.type =类型;
        this.start =启动;
        this.data =数据;
    }

    @覆盖
    公共字符串的toString(){
        返回类型+[+数据+];
    }
}
 

要简化符号化,让我们创建一个正则表达式读取输入字符串的下一个标记:

 私有静态最后图案标记=
        Pattern.compile((\\ S +)|(与)|(OR)|(=)|(\\()|(\\))|(\\瓦特+)| \'([^ \'] + )\');
 

请注意,它有许多组,每个 TokenType 一组以相同的顺序(先来 WHITESPACE ,然后等)。最后分词方法:

 私有静态管理记号标记化(字符串输入)抛出ParseException的{
    匹配匹配= TOKENS.matcher(输入);
    名单<令牌>令牌=新的ArrayList<>();
    INT偏移= 0;
    TokenType []类型= TokenType.values​​();
    而(偏移!= input.length()){
        如果(!matcher.find()|| matcher.start()!=偏移){
            抛出新ParseException的(在意外的标记+偏移,偏移量);
        }
        的for(int i = 0; I< types.length;我++){
            如果(matcher.group第(i + 1)!= NULL){
                如果(类型[I]!= TokenType.WHITESPACE)
                    tokens.add(新令牌(类型[I],偏移,matcher.group第(i + 1)));
                打破;
            }
        }
        偏移量= matcher.end();
    }
    tokens.add(新令牌(TokenType.EOF,input.length(),));
    返回新的管理记号(标记);
}
 

我用 java.text.ParseException 。在这里,我们应用正则表达式匹配器,直到输入的结束。如果它不在当前位置匹配,我们抛出异常。否则,我们找找不到匹配的组,创建它忽视了 WHITESPACE 标记的令牌。最后,我们添加了一个 EOF 标记,表示输入的结束。返回的结果为特殊的TokenStream 对象。这里的的TokenStream 类,它会帮我们做了解析:

 私有静态类的TokenStream {
    最后的名单,其中,令牌>令牌;
    INT偏移= 0;

    公共管理记号(表<令牌>令牌){
        this.tokens =令牌;
    }

    //消耗给定类型的下一个标记(抛出异常,如果类型不同)
    公共令牌消费(TokenType型)抛出ParseException的{
        令牌标记= tokens.get(偏移++);
        如果(token.type!=型){
            抛出新ParseException的(意外标记为+ token.start
                    +:+令牌+(一直在寻找的+类型+),
                    token.start);
        }
        返回令牌;
    }

    //消耗给定类型的令牌(返回null,如果类型不同,不提前)
    公共令牌consumeIf(TokenType型){
        令牌标记= tokens.get(补偿);
        如果(token.type ==型){
            偏移++;
            返回令牌;
        }
        返回null;
    }

    @覆盖
    公共字符串的toString(){
        返回tokens.toString();
    }
}
 

因此​​,我们有一个标记生成器,hoorah。您可以测试它现在使用的System.out.println(标记化(Acct1 ='Y'AND(Acct2 ='n'或Acct3 ='N')));

现在,让我们写这将创造我们的前pression树形重新presentation解析器。先在接口 Expr的所有的树节点:

 公共接口Expr的{
    公共布尔评估(地图<字符串,字符串>数据);
}
 

使用它的唯一方法评价前pression对于给定的数据集,如果数据集匹配返回true。

最基本的EX pression是 EqualsExpr 这就像 Acct1 =Y'Y'= Acct1

 私有静态类EqualsExpr实现Expr的{
    私人最终字符串标识,文字;

    公共EqualsExpr(管理记号流)抛出ParseException的{
        令牌标记= stream.consumeIf(TokenType.IDENTIFIER);
        如果(令牌!= NULL){
            this.identifier = token.data;
            stream.consume(TokenType.EQUALS);
            this.literal = stream.consume(TokenType.LITERAL).DATA;
        } 其他 {
            this.literal = stream.consume(TokenType.LITERAL).DATA;
            stream.consume(TokenType.EQUALS);
            this.identifier = stream.consume(TokenType.IDENTIFIER).DATA;
        }
    }

    @覆盖
    公共字符串的toString(){
        返回标识符+=+文字+';
    }

    @覆盖
    公共布尔评估(地图<字符串,字符串>数据){
        返回literal.equals(data.get(标识));
    }
}
 

的toString()的方法是只为信息,您可以将其删除。

接下来,我们将定义的Subexpr 类,或者是 EqualsExpr 或更复杂的东西在括号(如果我们看到括号):

 私有静态类的Subexpr实现Expr的{
    私人最终Expr的孩子;

    公众的Subexpr(管理记号流)抛出ParseException的{
        如果(stream.consumeIf(TokenType.LEFT_PAREN)!= NULL){
            孩子=新OrExpr(流);
            stream.consume(TokenType.RIGHT_PAREN);
        } 其他 {
            孩子=新EqualsExpr(流);
        }
    }

    @覆盖
    公共字符串的toString(){
        回报(+儿童+);
    }

    @覆盖
    公共布尔评估(地图<字符串,字符串>数据){
        返回child.evaluate(数据);
    }
}
 

接下来是 AndExpr 这是一组由和操作符:

 私有静态类AndExpr实现Expr的{
    私人最终名单,其中,Expr的>孩子=新的ArrayList<>();

    公共AndExpr(管理记号流)抛出ParseException的{
        做 {
            children.add(新的Subexpr(流));
        }而(stream.consumeIf(TokenType.AND)!= NULL);
    }

    @覆盖
    公共字符串的toString(){
        。返回children.stream()图(对象::的toString).collect(Collectors.joining(和));
    }

    @覆盖
    公共布尔评估(地图<字符串,字符串>数据){
        对于(表达式的孩子:孩子){
            如果(!child.evaluate(数据))
                返回false;
        }
        返回true;
    }
}
 

我使用了的toString 为了简便起见,Java的8个流API。如果你不能使用Java-8,你可以重写它与循环或删除的toString 完全

最后,我们定义 OrExpr 这是一组 AndExpr 加入(通常的优先级低于)。这非常类似于 AndExpr

 私有静态类OrExpr实现Expr的{
    私人最终名单,其中,Expr的>孩子=新的ArrayList<>();

    公共OrExpr(管理记号流)抛出ParseException的{
        做 {
            children.add(新AndExpr(流));
        }而(stream.consumeIf(TokenType.OR)!= NULL);
    }

    @覆盖
    公共字符串的toString(){
        返回children.stream()图(对象::的toString).collect(Collectors.joining(OR))。
    }

    @覆盖
    公共布尔评估(地图<字符串,字符串>数据){
        对于(表达式的孩子:孩子){
            如果(child.evaluate(数据))
                返回true;
        }
        返回false;
    }
}
 

和最后的解析方法:

 公共静态Expr的解析(管理记号流)抛出ParseException的{
    OrExpr EXPR =新OrExpr(流);
    stream.consume(TokenType.EOF); //确保我们解析整个输入
    返回expr的;
}
 

所以,你可以分析你的前pressions获得 Expr的目标,然后评估他们对你的CSV文件的行。我认为你有能力来分析CSV行插入地图<字符串,字符串> 。下面是使用例子:

 地图<字符串,字符串>数据=新的HashMap<>();
data.put(Acct1,Y);
data.put(Acct2,N);
data.put(Acct3,Y);
data.put(Acct4,N);

EXPR EXPR =解析(标记化(Acct1 ='Y'AND(Acct2 ='Y'或Acct3 ='Y')));
的System.out.println(expr.evaluate(数据)); // 真正
EXPR =解析(标记化(Acct1 ='n'或'Y'= Acct2和Acct3 =Y));
的System.out.println(expr.evaluate(数据)); // 假
 

I have an incoming records filter stored with the logical clause as given below.

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' )

My data input to this clause will be from Csv file as below.

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,

I will have to filter out the records in the file based on the conditions defined in the clause. This is a example of one simple clause but there will be more inner conditions than this and the clause can be changed whenever the user want and there will be 10 such clauses the records has to pass through sequentially.

So I am looking for a way to dynamically interpret the clause and apply it on the incoming records. Please provide me your suggestions about how to design/ any example if available.

解决方案

Here's the complete solution which does not include third-party libraries like ANTLR or JavaCC. Note that while it's extensible, its capabilities are still limited. If you want to create much more complex expressions, you'd better use grammar generator.

First, let's write a tokenizer which splits the input string to the tokens. Here's the token types:

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

The token class itself:

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 + "]";
    }
}

To simplify the tokenization let's create a regexp which reads the next token from the input string:

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

Note that it has many groups, one group per TokenType in the same order (first comes WHITESPACE, then AND and so on). Finally the tokenizer method:

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

I'm using java.text.ParseException. Here we apply the regex Matcher till the end of the input. If it doesn't match at the current position, we throw an exception. Otherwise we look for found matching group and create a token from it ignoring the WHITESPACE tokens. Finally we add a EOF token which indicates the end of the input. The result is returned as special TokenStream object. Here's the TokenStream class which will help us to do the parsing:

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

So we have a tokenizer, hoorah. You can test it right now using System.out.println(tokenize("Acct1 = 'Y' AND (Acct2 = 'N' OR Acct3 = 'N')"));

Now let's write the parser which will create the tree-like representation of our expression. First the interface Expr for all the tree nodes:

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

Its only method used to evaluate the expression for given data set and return true if data set matches.

The most basic expression is the EqualsExpr which is like Acct1 = 'Y' or '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));
    }
}

The toString() method is just for information, you can remove it.

Next we will define the SubExpr class which is either EqualsExpr or something more complex in parentheses (if we see the parenthesis):

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

Next is AndExpr which is a set of SubExpr expressions joined by AND 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;
    }
}

I use Java-8 Stream API in the toString for brevity. If you cannot use Java-8, you may rewrite it with the for loop or remove toString completely.

Finally we define OrExpr which is a set of AndExpr joined by OR (usually OR has lower priority than AND). It's very similar to 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;
    }
}

And the final parse method:

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

So you can parse your expressions to get the Expr objects, then evaluate them against the rows of your CSV file. I assume that you're capable to parse the CSV row into the Map<String, String>. Here's usage example:

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