Search code examples
pythondictionary

Check if dictionary key, value pairs meet user-defined search conditions


I have a dictionary of the form:

dictionary = {'a':25, 'ab':3.3, '(cd)': 4, 'ef':35, 'gh':12.2, etc.}

a user enters a comma-separated phrase (search string) like:

'a > 5, 0 < (cd) < 6, ef < 35'

Above we have three conditions, but it could be any number of conditions: What I would like is to check if my dictionary satisfies ALL search conditions.

 if dict meets all conditions:
     do somthing
 else:
     do something else

I can take the search string, do split(','), loop over each condition, extract the key, the comparison operator (< or >), and the number, and check if my dictionary meets all conditions. In the example above dictionary['a'] > 5 is met, 0 < dictionary['(cd)'] < 6 is also met, but dictionary['ef'] < 35 is not met.

Is there a quicker and more elegant way to do this? A better and smarter way to go from 'a > 5' to dictionary['a'] > 5? For example, can I convert my dictionary to a Pandas data frame, and do some checks? Is there an evaluation method, or something similar in Python aside from the brute force method that I am trying?


Solution

  • It isn't very hard to build a parser for this. You can use the operator package to handle the conditions, and other than that, it's just a bunch of formatting and looping data.

    import operator, re
    
    # dictionary of operators
    operators = {'<=':operator.le,
                 '>=':operator.ge,
                 '>' :operator.gt,
                 '<' :operator.lt,
                 '==':operator.eq,
                 '!=':operator.ne
                }
            
    # format operators keys for regex group
    _oper = '|'.join(map(re.escape, operators.keys()))
            
    # regex
    oper   = re.compile(fr'\s*({_oper})\s*').split
    number = re.compile(r'-?\d*(\.\d+)?').fullmatch
    string = re.compile(r'("|\')(?P<str>.*)\1').fullmatch
    
    def cast(value:str) -> float|int|bool|str|None:
        out = None
        
        if (v := value.lower()) in ('true', 'false'):
            out = v == "true"
        elif m := string(value):
            out = m.group('str')
        elif m := number(value):
            out = (int,float)['.' in v](v)
            
        return out
    
    def check_conditions(data:dict, conditions:str) -> bool:
        # stores condition results
        facts = []
    
        for cond in conditions.split(','):
            # stores values and operators
            v, o = [], []
            
            # parse values and operators
            for c in oper(cond):
                if (c := c.strip()) in operators:
                    # store operator function
                    o.append(operators.get(c))
                else:
                    # try data[c] else cast c
                    v.append(data.get(c, cast(c)))
                  
            # make sure values length is one more than operators              
            if (len(v) - len(o)) != 1:
                raise ValueError
                
            # call all operator functions on values and store results
            facts += [o[i](*v[i:i+2]) for i in range(len(o))]
        
        return all(facts)
    

    usage

    data  = {'a':25, 'ab':3.3, '(cd)': 4, 'ef':35, 'gh':.2, 'ij':"hello", 'kl':False, "mn":None}
    
    conds = 'mn==None, a>5, 0<(cd)<5, 35==ef!=34>12<20, ij==\'hello\''  
    print(check_conditions(data, conds)) #True