Skip to content

Commit c8be207

Browse files
Merge pull request #49 from NHSDigital/feature/eli-152-operators-impl
ELI-152 Implement full list of available operators for the Rule Engine
2 parents 37e3b3c + e2f845e commit c8be207

File tree

3 files changed

+797
-35
lines changed

3 files changed

+797
-35
lines changed

src/eligibility_signposting_api/model/rules.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,40 @@ class RuleType(str, Enum):
3535

3636
class RuleOperator(str, Enum):
3737
equals = "="
38-
ne = "!="
39-
lt = "<"
40-
lte = "<="
4138
gt = ">"
39+
lt = "<"
40+
ne = "!="
4241
gte = ">="
43-
year_gt = "Y>"
42+
lte = "<="
43+
contains = "contains"
44+
not_contains = "not_contains"
45+
starts_with = "starts_with"
46+
not_starts_with = "not_starts_with"
47+
ends_with = "ends_with"
48+
is_in = "in"
4449
not_in = "not_in"
45-
date_gte = "D>="
4650
member_of = "MemberOf"
51+
not_member_of = "NotaMemberOf"
52+
is_null = "is_null"
53+
is_not_null = "is_not_null"
54+
between = "between"
55+
not_between = "not_between"
56+
is_empty = "is_empty"
57+
is_not_empty = "is_not_empty"
58+
is_true = "is_true"
59+
is_false = "is_false"
60+
day_lte = "D<="
61+
day_lt = "D<"
62+
day_gte = "D>="
63+
day_gt = "D>"
64+
week_lte = "W<="
65+
week_lt = "W<"
66+
week_gte = "W>="
67+
week_gt = "W>"
68+
year_lte = "Y<="
69+
year_lt = "Y<"
70+
year_gte = "Y>="
71+
year_gt = "Y>"
4772

4873

4974
class RuleAttributeLevel(str, Enum):

src/eligibility_signposting_api/services/eligibility_services.py

Lines changed: 113 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,23 +80,126 @@ def get_attribute_value(iteration_rule: IterationRule, person_data: list[dict[st
8080
return attribute_value
8181

8282
@staticmethod
83-
def evaluate_rule(iteration_rule: IterationRule, attribute_value: Any) -> bool: # noqa: PLR0911, ANN401
83+
def evaluate_rule(iteration_rule: IterationRule, attribute_value: Any) -> bool: # noqa: PLR0911, ANN401, PLR0912, C901, PLR0915
8484
match iteration_rule.operator:
8585
case RuleOperator.equals:
8686
return attribute_value == iteration_rule.comparator
87-
case RuleOperator.ne:
88-
return attribute_value != iteration_rule.comparator
89-
case RuleOperator.lt:
90-
return int(attribute_value or 0) < int(iteration_rule.comparator)
91-
case RuleOperator.lte:
92-
return int(attribute_value or 0) <= int(iteration_rule.comparator)
9387
case RuleOperator.gt:
94-
return int(attribute_value or 0) > int(iteration_rule.comparator)
88+
return bool(attribute_value) and int(attribute_value) > int(iteration_rule.comparator)
89+
case RuleOperator.lt:
90+
return bool(attribute_value) and int(attribute_value) < int(iteration_rule.comparator)
91+
case RuleOperator.ne:
92+
return bool(attribute_value) and attribute_value != iteration_rule.comparator
9593
case RuleOperator.gte:
96-
return int(attribute_value or 0) >= int(iteration_rule.comparator)
94+
return bool(attribute_value) and int(attribute_value) >= int(iteration_rule.comparator)
95+
case RuleOperator.lte:
96+
return bool(attribute_value) and int(attribute_value) <= int(iteration_rule.comparator)
97+
case RuleOperator.contains:
98+
return attribute_value and iteration_rule.comparator in str(attribute_value)
99+
case RuleOperator.not_contains:
100+
return iteration_rule.comparator not in str(attribute_value)
101+
case RuleOperator.starts_with:
102+
return str(attribute_value).startswith(iteration_rule.comparator)
103+
case RuleOperator.not_starts_with:
104+
return not str(attribute_value).startswith(iteration_rule.comparator)
105+
case RuleOperator.ends_with:
106+
return str(attribute_value).endswith(iteration_rule.comparator)
107+
case RuleOperator.is_in:
108+
comparators = str(iteration_rule.comparator).split(",")
109+
return str(attribute_value) in comparators
110+
case RuleOperator.not_in:
111+
comparators = str(iteration_rule.comparator).split(",")
112+
return str(attribute_value) not in comparators
113+
case RuleOperator.member_of:
114+
attribute_values = str(attribute_value).split(",")
115+
return iteration_rule.comparator in attribute_values
116+
case RuleOperator.not_member_of:
117+
attribute_values = str(attribute_value).split(",")
118+
return iteration_rule.comparator not in attribute_values
119+
case RuleOperator.is_null:
120+
return attribute_value in (None, "")
121+
case RuleOperator.is_not_null:
122+
return attribute_value not in (None, "")
123+
case RuleOperator.between:
124+
if attribute_value in (None, ""):
125+
return False
126+
low_comparator_str, high_comparator_str = str(iteration_rule.comparator).split(",")
127+
low_comparator = min(int(low_comparator_str), int(high_comparator_str))
128+
high_comparator = max(int(low_comparator_str), int(high_comparator_str))
129+
return low_comparator <= int(attribute_value) <= high_comparator
130+
case RuleOperator.not_between:
131+
if attribute_value in (None, ""):
132+
return False
133+
low_comparator_str, high_comparator_str = str(iteration_rule.comparator).split(",")
134+
low_comparator = min(int(low_comparator_str), int(high_comparator_str))
135+
high_comparator = max(int(low_comparator_str), int(high_comparator_str))
136+
return int(attribute_value) < low_comparator or int(attribute_value) > high_comparator
137+
case RuleOperator.is_empty:
138+
return attribute_value is None or all(item.strip() == "" for item in attribute_value.split(","))
139+
case RuleOperator.is_not_empty:
140+
return attribute_value is not None and any(item.strip() != "" for item in attribute_value.split(","))
141+
case RuleOperator.is_true:
142+
return attribute_value is True
143+
case RuleOperator.is_false:
144+
return attribute_value is False
145+
case RuleOperator.day_lte:
146+
attribute_date = datetime.strptime(str(attribute_value), "%Y%m%d") if attribute_value else None # noqa: DTZ007
147+
today_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # noqa: DTZ002
148+
cutoff = today_date + relativedelta(days=int(iteration_rule.comparator))
149+
return (attribute_date <= cutoff) if attribute_date else False
150+
case RuleOperator.day_lt:
151+
attribute_date = datetime.strptime(str(attribute_value), "%Y%m%d") if attribute_value else None # noqa: DTZ007
152+
today_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # noqa: DTZ002
153+
cutoff = today_date + relativedelta(days=int(iteration_rule.comparator))
154+
return (attribute_date < cutoff) if attribute_date else False
155+
case RuleOperator.day_gte:
156+
attribute_date = datetime.strptime(str(attribute_value), "%Y%m%d") if attribute_value else None # noqa: DTZ007
157+
today_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # noqa: DTZ002
158+
cutoff = today_date + relativedelta(days=int(iteration_rule.comparator))
159+
return (attribute_date >= cutoff) if attribute_date else False
160+
case RuleOperator.day_gt:
161+
attribute_date = datetime.strptime(str(attribute_value), "%Y%m%d") if attribute_value else None # noqa: DTZ007
162+
today_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # noqa: DTZ002
163+
cutoff = today_date + relativedelta(days=int(iteration_rule.comparator))
164+
return (attribute_date > cutoff) if attribute_date else False
165+
case RuleOperator.week_lte:
166+
attribute_date = datetime.strptime(str(attribute_value), "%Y%m%d") if attribute_value else None # noqa: DTZ007
167+
today_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # noqa: DTZ002
168+
cutoff = today_date + relativedelta(weeks=int(iteration_rule.comparator))
169+
return (attribute_date <= cutoff) if attribute_date else False
170+
case RuleOperator.week_lt:
171+
attribute_date = datetime.strptime(str(attribute_value), "%Y%m%d") if attribute_value else None # noqa: DTZ007
172+
today_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # noqa: DTZ002
173+
cutoff = today_date + relativedelta(weeks=int(iteration_rule.comparator))
174+
return (attribute_date < cutoff) if attribute_date else False
175+
case RuleOperator.week_gte:
176+
attribute_date = datetime.strptime(str(attribute_value), "%Y%m%d") if attribute_value else None # noqa: DTZ007
177+
today_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # noqa: DTZ002
178+
cutoff = today_date + relativedelta(weeks=int(iteration_rule.comparator))
179+
return (attribute_date >= cutoff) if attribute_date else False
180+
case RuleOperator.week_gt:
181+
attribute_date = datetime.strptime(str(attribute_value), "%Y%m%d") if attribute_value else None # noqa: DTZ007
182+
today_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # noqa: DTZ002
183+
cutoff = today_date + relativedelta(weeks=int(iteration_rule.comparator))
184+
return (attribute_date > cutoff) if attribute_date else False
185+
case RuleOperator.year_lte:
186+
attribute_date = datetime.strptime(str(attribute_value), "%Y%m%d") if attribute_value else None # noqa: DTZ007
187+
today_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # noqa: DTZ002
188+
cutoff = today_date + relativedelta(years=int(iteration_rule.comparator))
189+
return (attribute_date <= cutoff) if attribute_date else False
190+
case RuleOperator.year_lt:
191+
attribute_date = datetime.strptime(str(attribute_value), "%Y%m%d") if attribute_value else None # noqa: DTZ007
192+
today_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # noqa: DTZ002
193+
cutoff = today_date + relativedelta(years=int(iteration_rule.comparator))
194+
return (attribute_date < cutoff) if attribute_date else False
195+
case RuleOperator.year_gte:
196+
attribute_date = datetime.strptime(str(attribute_value), "%Y%m%d") if attribute_value else None # noqa: DTZ007
197+
today_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # noqa: DTZ002
198+
cutoff = today_date + relativedelta(years=int(iteration_rule.comparator))
199+
return (attribute_date >= cutoff) if attribute_date else False
97200
case RuleOperator.year_gt:
98201
attribute_date = datetime.strptime(str(attribute_value), "%Y%m%d") if attribute_value else None # noqa: DTZ007
99-
today = datetime.today() # noqa: DTZ002
202+
today = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # noqa: DTZ002
100203
cutoff = today + relativedelta(years=int(iteration_rule.comparator))
101204
return (attribute_date > cutoff) if attribute_date else False
102205
case _:

0 commit comments

Comments
 (0)