1
\$\begingroup\$

I have a set of parameters, and I need to get the expression:

So I got this parameters:

filters = {
  "type": 'human',
  "age": [{'min': 4, 'max': 8}, {'min': 15, 'max': 30}],
  "size": [{'min': 60}],
}

And my goal is to obtain this string from it:

type == 'human' and ((age >= 4 and age <= 8) or (age >= 15 and age <= 30)) and size >= 60

But when I am writing my script to do so, I keep having small details that bother me like the position of the 'or' or 'and' or the parenthesis. And i think it really hit the readability here. So I thought maybe there was a better way, but searching Google only shows me the inverse of what I want, that is transforming expression to expression tree.

I am trying to generate a string expression from a set of data (for a filter).

So if someone have a better idea, (or algo, or ...)

Here is my script:

#!/usr/bin/python3

# goal : type == 'human' and ((age >= 4 and age <= 8) or (age >= 15 and age <= 30)) and size >= 60
filters = {
  "type": 'human',
  "age": [{'min': 4, 'max': 8}, {'min': 15, 'max': 30}],
  "size": [{'min': 60}],
}

s = ''
type_first = True
for key, value in filters.items():
  if type_first:
    type_first = False
    s += "("
  else:
    s += " and ("
  value_first = True
  if(isinstance(value, str)):
    s += "%s == %s" % (key, value)
  elif(isinstance(value, list)):
    if value_first:
      value_first = False
    else:
      s += " and"
    dict_first = True
    for dict_ in value:
      if dict_first:
        dict_first = False
        s += "("
      else:
        s += " or ("  
      if dict_.get('min', 0):
        s += "%s >= %s" % (key, dict_['min'])
        if dict_.get('max', 0):
          s += " and "
      if dict_.get('max', 0):
        s += "%s <= %s" % (key, dict_['max'])
      s += ")"
  s += ")"

print(s)
\$\endgroup\$
  • 1
    \$\begingroup\$ what if you want to make ((age >= 4 and age <= 8) and (age >= 15 and age <= 30))? \$\endgroup\$ – Azat Ibrakov Nov 8 '18 at 16:48
  • \$\begingroup\$ Your specification says that you want type == 'human', but your code actually prints type == human. Please clarify which one you want? \$\endgroup\$ – 200_success Nov 9 '18 at 5:57
  • \$\begingroup\$ @200_success The code I put was an example (my real problem is not in python), so I didnt put a lot on it except to show the problem, and you gave a good response, but I forgot in python there was the 'join' who make it easy, but I will just implement my own. Thanks. \$\endgroup\$ – KlossOne Nov 9 '18 at 8:50
2
\$\begingroup\$

You have a bug, which is that if a 'min' or a 'max' value is 0, then it will be ignored.

For readability,

def filter_expr(filters):
    def range_expr(key, min_max):
        lb = f'{key} >= {min_max["min"]}' if 'min' in min_max else None
        ub = f'{key} <= {min_max["max"]}' if 'max' in min_max else None
        return f'({lb} and {ub})' if (ub and lb) else lb or ub

    def ranges_expr(key, values):
        if len(values) == 1:
            return range_expr(key, values[0])
        else:
            return '(' + ' or '.join(
                range_expr(key, min_max) for min_max in values
            ) + ')'

    return ' and '.join(
        f"{key} = '{value}'" if isinstance(value, str) else 
        ranges_expr(key, value)
        for key, value in filters.items()
    )

# goal : type == 'human' and ((age >= 4 and age <= 8) or (age >= 15 and age <= 30)) and size >= 60
filters = {
  "type": 'human',
  "age": [{'min': 4, 'max': 8}, {'min': 15, 'max': 30}],
  "size": [{'min': 60}],
}
print(filter_expr(filters))
\$\endgroup\$
2
\$\begingroup\$
  • Please note that I am not actually reviewing your code, but guiding you towards hopefully better design, so that you can fix the issues yourself
  • In that same vein, the code snippets I am including do not contain a full solution, but only the hints towards it.

These kinds of transformations are very easy if you start to decompose them.

For example, you have two different rules for dealing with string data type and list data type, so let's create two different entities to deal with them.

(Here I chose functions as top-level entities, you can create classes too)

def convert_string(key, value):
    return f"{key} == '{value}'"   # I am assuming you want value in quotes

Similarly, define one for lists. (Hint: You should define one entity for dict and list function should only call and compose dict function)

How do you choose which entity to call? Simple, call another entity.

def choose_function(key, value):
    # Your logic here
    # returns func

And finally, a composing function, which is also your main function.

def convert(params):
    str_list = []
    for key, value in params.items():
        str_list.append(choose_func(key, value)(key, value))
    return ' and '.join(str_list)}

Why it would be better than your solution?

  • It is much easier to test. Your problem statement is pretty much easy, but these kinds of problems could become very big with lots of complex rules in the long run, where unit testability gives you the confidence to make changes

  • You divide the problem statement into individual sub-pieces and another sub-problem deal with how to merge those solutions. That would eliminate lots of your _first variables. It also makes it easier in the long run.

  • Code readability

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.