1818
1919@dataclass
2020class Operator (BaseMatcher [str | None ], ABC ):
21+ """An operator compares some person's data attribute - date of birth, postcode, flags or so on - against a value
22+ specified in a rule."""
23+
24+ ITEM_DEFAULT_PATTERN : ClassVar [str ] = r"(?P<rule_value>[^\[]+)\[\[NVL:(?P<item_default>[^\]]+)\]\]"
25+
2126 rule_value : str
27+ item_default : str | None = None
28+
29+ def __post_init__ (self ) -> None :
30+ if self .rule_value and (match := re .match (self .ITEM_DEFAULT_PATTERN , self .rule_value )):
31+ self .rule_value = match .group ("rule_value" )
32+ self .item_default = match .group ("item_default" )
2233
2334 @abstractmethod
2435 def _matches (self , item : str | None ) -> bool : ...
@@ -28,6 +39,8 @@ def describe_to(self, description: Description) -> None:
2839
2940
3041class OperatorRegistry :
42+ """Operators are registered and made available for retrieval here for each RuleOperator."""
43+
3144 registry : ClassVar [dict [RuleOperator , type [Operator ]]] = {}
3245
3346 @staticmethod
@@ -50,6 +63,7 @@ class ScalarOperator(Operator, ABC):
5063 comparator : ClassVar [Callable [[str | None , str | None ], bool ]]
5164
5265 def _matches (self , item : str | None ) -> bool :
66+ item = item if item is not None else self .item_default
5367 data_comparator = cast ("Callable[[str|int, str|int], bool]" , self .comparator )
5468 person_data : str | int
5569 rule_value : str | int
@@ -100,37 +114,43 @@ def describe_to(self, description: Description) -> None:
100114@OperatorRegistry .register (RuleOperator .contains )
101115class Contains (Operator ):
102116 def _matches (self , item : str | None ) -> bool :
117+ item = item if item is not None else self .item_default
103118 return bool (item ) and self .rule_value in str (item )
104119
105120
106121@OperatorRegistry .register (RuleOperator .not_contains )
107122class NotContains (Operator ):
108123 def _matches (self , item : str | None ) -> bool :
124+ item = item if item is not None else self .item_default
109125 return self .rule_value not in str (item )
110126
111127
112128@OperatorRegistry .register (RuleOperator .starts_with )
113129class StartsWith (Operator ):
114130 def _matches (self , item : str | None ) -> bool :
131+ item = item if item is not None else self .item_default
115132 return str (item ).startswith (self .rule_value )
116133
117134
118135@OperatorRegistry .register (RuleOperator .not_starts_with )
119136class NotStartsWith (Operator ):
120137 def _matches (self , item : str | None ) -> bool :
138+ item = item if item is not None else self .item_default
121139 return not str (item ).startswith (self .rule_value )
122140
123141
124142@OperatorRegistry .register (RuleOperator .ends_with )
125143class EndsWith (Operator ):
126144 def _matches (self , item : str | None ) -> bool :
145+ item = item if item is not None else self .item_default
127146 return str (item ).endswith (self .rule_value )
128147
129148
130149@OperatorRegistry .register (RuleOperator .is_in )
131150@OperatorRegistry .register (RuleOperator .member_of )
132151class IsIn (Operator ):
133152 def _matches (self , item : str | None ) -> bool :
153+ item = item if item is not None else self .item_default
134154 comparators = str (self .rule_value ).split ("," )
135155 return str (item ) in comparators
136156
@@ -139,6 +159,7 @@ def _matches(self, item: str | None) -> bool:
139159@OperatorRegistry .register (RuleOperator .not_member_of )
140160class NotIn (Operator ):
141161 def _matches (self , item : str | None ) -> bool :
162+ item = item if item is not None else self .item_default
142163 comparators = str (self .rule_value ).split ("," )
143164 return str (item ) not in comparators
144165
@@ -156,8 +177,12 @@ def _matches(self, item: str | None) -> bool:
156177
157178
158179class RangeOperator (Operator , ABC ):
159- def __init__ (self , rule_value : str ) -> None :
160- super ().__init__ (rule_value = rule_value )
180+ low_comparator : int
181+ high_comparator : int
182+
183+ def __post_init__ (self ) -> None :
184+ super ().__post_init__ ()
185+
161186 low_comparator_str , high_comparator_str = str (self .rule_value ).split ("," )
162187 self .low_comparator = min (int (low_comparator_str ), int (high_comparator_str ))
163188 self .high_comparator = max (int (low_comparator_str ), int (high_comparator_str ))
@@ -166,6 +191,7 @@ def __init__(self, rule_value: str) -> None:
166191@OperatorRegistry .register (RuleOperator .is_between )
167192class Between (RangeOperator ):
168193 def _matches (self , item : str | None ) -> bool :
194+ item = item if item is not None else self .item_default
169195 if item in (None , "" ):
170196 return False
171197 return self .low_comparator <= int (item ) <= self .high_comparator
@@ -174,6 +200,7 @@ def _matches(self, item: str | None) -> bool:
174200@OperatorRegistry .register (RuleOperator .is_not_between )
175201class NotBetween (RangeOperator ):
176202 def _matches (self , item : str | None ) -> bool :
203+ item = item if item is not None else self .item_default
177204 if item in (None , "" ):
178205 return False
179206 return not self .low_comparator <= int (item ) <= self .high_comparator
@@ -204,8 +231,17 @@ def _matches(self, item: str | None) -> bool:
204231
205232
206233class DateOperator (Operator , ABC ):
234+ OFFSET_PATTERN : ClassVar [str ] = r"(?P<rule_value>[^\[]+)\[\[OFFSET:(?P<offset>\d{8})\]\]"
207235 delta_type : ClassVar [str ]
208236 comparator : ClassVar [Callable [[date , date ], bool ]]
237+ offset : date | None = None
238+
239+ def __post_init__ (self ) -> None :
240+ super ().__post_init__ ()
241+
242+ if self .rule_value and (match := re .match (self .OFFSET_PATTERN , self .rule_value )):
243+ self .rule_value = match .group ("rule_value" )
244+ self .offset = datetime .strptime (match .group ("offset" ), "%Y%m%d" ).replace (tzinfo = UTC ).date ()
209245
210246 @property
211247 def today (self ) -> date :
@@ -219,9 +255,10 @@ def get_attribute_date(item: str | None) -> date | None:
219255 def cutoff (self ) -> date :
220256 delta = relativedelta ()
221257 setattr (delta , self .delta_type , int (self .rule_value ))
222- return self .today + delta
258+ return ( self .offset if self . offset else self . today ) + delta
223259
224260 def _matches (self , item : str | None ) -> bool :
261+ item = item if item is not None else self .item_default
225262 if attribute_date := self .get_attribute_date (item ):
226263 date_comparator = cast ("Callable[[date, date], bool]" , self .comparator )
227264 return date_comparator (attribute_date , self .cutoff )
0 commit comments