Open gemerden opened 7 years ago
To add some more context, the new test i wrote are:
class TestCallability(unittest.TestCase):
def test_and(self):
algebra = CallableBooleanAlgebra()
exp = algebra.parse("a&b&c")
for a in [True, False]:
for b in [True, False]:
for c in [True, False]:
self.assertEqual(exp(a=a, b=b, c=c), a and b and c)
def test_or(self):
algebra = CallableBooleanAlgebra()
exp = algebra.parse("a|b|c")
for a in [True, False]:
for b in [True, False]:
for c in [True, False]:
self.assertEqual(exp(a=a, b=b, c=c), a or b or c)
def test_not(self):
algebra = CallableBooleanAlgebra()
exp = algebra.parse("!a")
for a in [True, False]:
self.assertEqual(exp(a=a), not a)
def test_symbol(self):
algebra = CallableBooleanAlgebra()
exp = algebra.parse("a")
for a in [True, False]:
self.assertEqual(exp(a=a), a)
def test_composite(self):
algebra = CallableBooleanAlgebra()
exp = algebra.parse("!(a|b&(a|!c))")
for a in [True, False]:
for b in [True, False]:
for c in [True, False]:
self.assertEqual(exp(a=a, b=b, c=c), not(a or b and (a or not c)))
@gemerden Thanks!
So my questions are: Is this functionality already there but did or completely miss it (making this a fun but somewhat useless exercise)?
So what you are doing is evaluate a boolean expression by passing custom True/False values for Symbols? Evaluation is missing entirely! so that would be a welcome addition.
If not, is this something to add to the baseclasses (AND, OR, NOT, Symbol) themselves (but i can imagine this complicating subclassing a bit),
I am not sure why it would complicate subclassing?
Is there any interest in adding this to the repo in some other way?
Sure. Do you mind crafting a PR with your additions?
Compliments for the tight implementation of boolean that made this relatively easy.
All the credits go to @bastikr !
Now what I like about your approach is that is uses the Python evaluation in a clever way and it looks like it avoids the pitfalls of mixing boolean.py's TRUE and FALSE with Python's True and False, correct?
I am not sure why it would complicate subclassing? To keep the evaluation consistent with the other changes made in the subclass, you might have to override
__call__
as well, even if you do not use evaluation (otherwise you wind up with a class that only half works, although you might just raise NotImplementedError, i guess).Now what I like about your approach is that is uses the Python evaluation in a clever way and it looks like it avoids the pitfalls of mixing boolean.py's TRUE and FALSE with Python's True and False, correct? I don't know what the role of TRUE and FALSE is, are they used as something like a constant Symbol? In that case, the
Symbol.__call__
method might (guessing here) need an extraif
statement.
I have a branch ready for a PR, but i am pretty sure i need to be a collaborator to create a branch/push/PR.
@gemerden no need to be a collaborator: you just fork push a branch in your forked repo and then submit a PR on that branch
If anyone is interested in a use case for evaluations, I used a somewhat customized version of boolean.py
with evaluations in one of my projects: https://github.com/gemerden/wildpath (pip install wildpath
):
# '|' is the OR operator
assert WildPath("start_time|end_time").get_in(agenda) == {"start_time": "10:00", "end_time": "11:00"}
# '&' is the AND operator
assert WildPath("start_*&*_time").get_in(agenda) == {"start_time": "10:00"}
# '!' is the NOT operator:
assert WildPath("!item?").get_in({"item1": "chair", "item2": "table", "count": 2}) == {"count": 2}
# parentheses can be used to indicate precedence:
assert WildPath("!(a|b)") != WildPath("!a|b")
WildPath
supports paths, as in : WildPath("route.stops.*.lat|lng").get_in(some_vehicle)
would give all geo-coordinates of the route of some vehicle.
I'm not quite sure if this is what you're after, but you can evaluate the expressions by 1) providing your own subclass of Symbol
with an implementation of simplify()
, 2) making sure to simplify the expressions before evaluating to boolean.
import boolean
class Foo(boolean.Symbol):
def simplify(self):
# Replace with a sensible conversion
if len(self.obj) > 3:
return self.TRUE
else:
return self.FALSE
algebra = boolean.BooleanAlgebra(Symbol_class=Foo)
print(bool(algebra.parse('foobar and baz', simplify=True)))
print(bool(algebra.parse('foobar or baz', simplify=True)))
Side note, I think the Expression.__bool__
method should try to simplify first before throwing an exception.
@jnikula as a simplification, we agreed that we should not be able to evaluate a boolean expression as a Python expression (hence why Expression.__bool__
raises an exception). The rationale is that combining the two logic systems is a bit Byzantine at times especially when you consider Python shortcutting (such as in a =
1 or 0where
aends up being always
1and
0` may never be evaluated).
So your approach makes sense alright: in subclassing Symbol you can provide exactly how you would like this evaluation to take place from a Python point of view if you need such thing. You can also of course subclass or tweak Expression.__bool__
too.
If anyone needs this kind of functionality, be warned that @jnikula's suggestion fails for certain expressions.
If you negate a symbol (ie: 'foobar and ~baz'), then NOT.demorgan() fails during algrebra.parse.simplify():
File ".../venv/lib/python3.8/site-packages/boolean/boolean.py", line 1088, in demorgan return op.dual(*(self.__class__(arg).cancel() for arg in op.args)) TypeError: '_TRUE' object is not callable
@gemerden's solution worked very well for me.
@JonGalarneau I'll have to take your word for it.
I ended up using Lark to implement a query language for my needs.
Maybe i missed something completely, but i could not figure out how to actually use the boolean expressions to boolean values to calculate the result, so i implemented something myself:
I have written basic tests and checked whether the original tests pass (with some basic modifications) by replacing BooleanAlgebra with CallableBooleanAlgebra (
import CallableBooleanAlgebra as BooleanAlgebra
).Compliments for the tight implementation of boolean that made this relatively easy.
So my questions are:
Also comments, improvements, etc. are very welcome!
Cheers, Lars