@@ -13,20 +13,30 @@ class ParsedToken:
1313 Example: "PERSON" or "TARGET"
1414 attribute_name : str
1515 Example: "POSTCODE" or "RSV"
16- attribute_value : int
16+ attribute_value : str | None
1717 Example: "LAST_SUCCESSFUL_DATE" if attribute_level is TARGET
18- format : str
18+ format : str | None
1919 Example: "%d %B %Y" if DATE formatting is used
20+ function_name : str | None
21+ Example: "ADD_DAYS" for derived value functions
22+ function_args : str | None
23+ Example: "91" for ADD_DAYS(91)
2024 """
2125
2226 attribute_level : str
2327 attribute_name : str
2428 attribute_value : str | None
2529 format : str | None
30+ function_name : str | None = None
31+ function_args : str | None = None
2632
2733
2834class TokenParser :
2935 MIN_TOKEN_PARTS = 2
36+ # Pattern for function calls like ADD_DAYS(91) - captures function name and args
37+ FUNCTION_PATTERN = re .compile (r":([A-Z_]+)\(([^()]*)\)" , re .IGNORECASE )
38+ # Pattern for DATE format - special case as it's already supported
39+ DATE_PATTERN = re .compile (r":DATE\(([^()]*)\)" , re .IGNORECASE )
3040
3141 @staticmethod
3242 def parse (token : str ) -> ParsedToken :
@@ -35,8 +45,15 @@ def parse(token: str) -> ParsedToken:
3545 Strip the surrounding [[ ]]
3646 Check for empty body after stripping, e.g., '[[]]'
3747 Check for empty parts created by leading/trailing dots or tokens with no dot
38- Check if the name contains a date format
48+ Check if the name contains a date format or function call
3949 Return a ParsedToken object
50+
51+ Supported formats:
52+ - [[PERSON.AGE]] - Simple person attribute
53+ - [[TARGET.COVID.LAST_SUCCESSFUL_DATE]] - Target attribute
54+ - [[PERSON.DATE_OF_BIRTH:DATE(%d %B %Y)]] - With date formatting
55+ - [[TARGET.COVID.NEXT_DOSE_DUE:ADD_DAYS(91)]] - Derived value function
56+ - [[TARGET.COVID.NEXT_DOSE_DUE:ADD_DAYS(91):DATE(%d %B %Y)]] - Function with date format
4057 """
4158
4259 token_body = token [2 :- 2 ]
@@ -53,14 +70,20 @@ def parse(token: str) -> ParsedToken:
5370 token_level = token_parts [0 ].upper ()
5471 token_name = token_parts [- 1 ]
5572
56- format_match = re .search (r":DATE\(([^()]*)\)" , token_name , re .IGNORECASE )
57- if not format_match and len (token_name .split (":" )) > 1 :
58- message = "Invalid token format."
59- raise ValueError (message )
73+ # Extract function call (e.g., ADD_DAYS(91))
74+ function_name , function_args = TokenParser ._extract_function (token_name )
6075
76+ # Extract date format
77+ format_match = TokenParser .DATE_PATTERN .search (token_name )
6178 format_str = format_match .group (1 ) if format_match else None
6279
63- last_part = re .sub (r":DATE\([^)]*\)" , "" , token_name , flags = re .IGNORECASE )
80+ # Validate format - if there's a colon but no valid pattern, it's invalid
81+ if not format_match and not function_name and len (token_name .split (":" )) > 1 :
82+ message = "Invalid token format."
83+ raise ValueError (message )
84+
85+ # Remove function and date patterns to get the clean attribute name
86+ last_part = TokenParser ._clean_attribute_name (token_name )
6487
6588 if len (token_parts ) == TokenParser .MIN_TOKEN_PARTS :
6689 name = last_part .upper ()
@@ -69,4 +92,42 @@ def parse(token: str) -> ParsedToken:
6992 name = token_parts [1 ].upper ()
7093 value = last_part .upper ()
7194
72- return ParsedToken (attribute_level = token_level , attribute_name = name , attribute_value = value , format = format_str )
95+ return ParsedToken (
96+ attribute_level = token_level ,
97+ attribute_name = name ,
98+ attribute_value = value ,
99+ format = format_str ,
100+ function_name = function_name ,
101+ function_args = function_args ,
102+ )
103+
104+ @staticmethod
105+ def _extract_function (token_name : str ) -> tuple [str | None , str | None ]:
106+ """Extract function name and arguments from token name.
107+
108+ Args:
109+ token_name: The last part of the token (e.g., 'NEXT_DOSE_DUE:ADD_DAYS(91)')
110+
111+ Returns:
112+ Tuple of (function_name, function_args) or (None, None) if no function
113+ """
114+ # Find all function matches (excluding DATE which is handled separately)
115+ for match in TokenParser .FUNCTION_PATTERN .finditer (token_name ):
116+ func_name = match .group (1 ).upper ()
117+ if func_name != "DATE" :
118+ return func_name , match .group (2 )
119+ return None , None
120+
121+ @staticmethod
122+ def _clean_attribute_name (token_name : str ) -> str :
123+ """Remove function calls and date formatting from token name.
124+
125+ Args:
126+ token_name: The raw token name with potential modifiers
127+
128+ Returns:
129+ Clean attribute name
130+ """
131+ # Remove date format and other function calls
132+ without_date = TokenParser .DATE_PATTERN .sub ("" , token_name )
133+ return TokenParser .FUNCTION_PATTERN .sub ("" , without_date )
0 commit comments