Skip to content

Commit e33b35f

Browse files
authored
Merge pull request #70 from rudycelekli/fix/workbook-condition-precedence-20261010
fix(workbook): honor AND precedence in mixed conditions
2 parents d67e9aa + 717f669 commit e33b35f

2 files changed

Lines changed: 35 additions & 9 deletions

File tree

‎apps/api/services/workbook/conditions.py‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- {status} == "active" → only run if status matches
1212
- {email} == "" AND {website} != "" → compound conditions
1313
- {email} == "" OR {phone} == "" → OR conditions
14+
- AND binds before OR in mixed conditions (parentheses are not supported)
1415
- true / false / always / never → literals
1516
1617
Column config example:
@@ -61,15 +62,12 @@ def evaluate_condition(
6162
values = _get_values(row_cells, columns_config)
6263

6364
# Parse syntax before resolving cells so their contents remain operand data.
64-
parts = _split_unquoted(condition, r" AND ")
65-
if len(parts) > 1:
66-
return all(_eval_single(p.strip(), values) for p in parts)
67-
68-
parts = _split_unquoted(condition, r" OR ")
69-
if len(parts) > 1:
70-
return any(_eval_single(p.strip(), values) for p in parts)
71-
72-
return _eval_single(condition, values)
65+
# OR separates conjunctions: AND binds more tightly. Keep both passes on
66+
# authored syntax so operators inside literals or cell data stay data.
67+
return any(
68+
all(_eval_single(part.strip(), values) for part in _split_unquoted(group, r" AND "))
69+
for group in _split_unquoted(condition, r" OR ")
70+
)
7371

7472

7573
def _split_unquoted(expr: str, pattern: str, maxsplit: int = 0, flags: int = 0) -> list[str]:
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Mixed column conditions use AND precedence over OR."""
2+
3+
import itertools
4+
5+
import pytest
6+
7+
from apps.api.services.workbook.conditions import evaluate_condition
8+
9+
10+
@pytest.mark.parametrize("a,b,c", list(itertools.product([False, True], repeat=3)))
11+
@pytest.mark.parametrize("expression,order", [
12+
('{a} == "yes" OR {b} == "yes" AND {c} == "yes"', "or-and"),
13+
('{a} == "yes" AND {b} == "yes" OR {c} == "yes"', "and-or"),
14+
])
15+
def test_mixed_conditions_follow_boolean_truth_table(a, b, c, expression, order):
16+
cells = {key: {"value": "yes" if value else "no"} for key, value in zip("abc", (a, b, c))}
17+
expected = (a or (b and c)) if order == "or-and" else ((a and b) or c)
18+
assert evaluate_condition(expression, cells, []) is expected
19+
20+
21+
def test_mixed_conditions_keep_operator_text_inside_literals_and_cells():
22+
cells = {"name": {"value": "Sales OR Marketing AND Research"}, "score": {"value": 10}}
23+
assert evaluate_condition(
24+
'{name} == "Sales OR Marketing AND Research" OR {score} > 20 AND {score} < 0', cells, []
25+
)
26+
assert not evaluate_condition(
27+
'{name} == "Other" OR {score} > 20 AND {name} == "Sales OR Marketing AND Research"', cells, []
28+
)

0 commit comments

Comments
 (0)