From 0844550d06f33f2e2eb61f58bbaf59bcadd17588 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Mon, 24 Nov 2025 11:25:33 -0600 Subject: [PATCH] feat(selector): add variable substitution and arithmetic to format() - Add {{variable}} syntax for element property references - Add arithmetic operators: +, -, *, / with proper precedence - Extend format() with optional element parameter - Variables work with all format functions (imperial_length, round, etc.) - Maintains full backward compatibility Example: format("imperial_length({{z}} / 2, 4)", element) --- .../ifcopenshell/util/selector.py | 138 +++++++++++++++--- 1 file changed, 117 insertions(+), 21 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index a2ce6fb467..818fbe9703 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -139,21 +139,32 @@ get_element_grammar = lark.Lark( ) format_grammar = lark.Lark( - """start: function + """start: expression - function: round | number | int | format_length | lower | upper | title | concat | substr | ESCAPED_STRING | NUMBER + ?expression: add_sub + ?add_sub: mul_div + | add_sub "+" mul_div -> add + | add_sub "-" mul_div -> subtract + ?mul_div: function + | mul_div "*" function -> multiply + | mul_div "/" function -> divide + + function: round | number | int | format_length | lower | upper | title | concat | substr | variable | ESCAPED_STRING | NUMBER | "(" expression ")" - round: "round(" function "," NUMBER ")" - number: "number(" function ["," ESCAPED_STRING ["," ESCAPED_STRING]] ")" - int: "int(" function ")" + variable: "{{" query_path "}}" + query_path: /[^}]+/ + + round: "round(" expression "," NUMBER ")" + number: "number(" expression ["," ESCAPED_STRING ["," ESCAPED_STRING]] ")" + int: "int(" expression ")" format_length: metric_length | imperial_length - metric_length: "metric_length(" function "," NUMBER "," NUMBER ")" - imperial_length: "imperial_length(" function "," NUMBER ["," ESCAPED_STRING "," ESCAPED_STRING ["," boolean]] ")" - lower: "lower(" function ")" - upper: "upper(" function ")" - title: "title(" function ")" - concat: "concat(" function ("," function)* ")" - substr: "substr(" function "," SIGNED_INT ["," SIGNED_INT] ")" + metric_length: "metric_length(" expression "," NUMBER "," NUMBER ")" + imperial_length: "imperial_length(" expression "," NUMBER ["," ESCAPED_STRING "," ESCAPED_STRING ["," boolean]] ")" + lower: "lower(" expression ")" + upper: "upper(" expression ")" + title: "title(" expression ")" + concat: "concat(" expression ("," expression)* ")" + substr: "substr(" expression "," SIGNED_INT ["," SIGNED_INT] ")" boolean: TRUE | FALSE TRUE: "true" | "True" | "TRUE" @@ -189,9 +200,83 @@ format_grammar = lark.Lark( class FormatTransformer(lark.Transformer): + def __init__(self, element=None): + """Initialize transformer with optional element for variable substitution""" + super().__init__() + self.element = element + def start(self, args): return args[0] + def expression(self, args): + return args[0] + + def variable(self, args): + """Handle variable substitution like {{z}} or {{Pset_Wall.FireRating}}""" + if self.element is None: + return "0" # Default value if no element context + + query_path = args[0] + try: + value = get_element_value(self.element, query_path) + if value is None: + return "0" + # Convert to string for further processing + return str(value) + except: + return "0" # Return default on error + + def query_path(self, args): + """Extract the query path from variable""" + return str(args[0]).strip() + + def add(self, args): + """Handle addition operation""" + left, right = args + try: + left_val = float(left) if left != "None" and left is not None else 0.0 + right_val = float(right) if right != "None" and right is not None else 0.0 + result = left_val + right_val + # Return integer if result has no decimal part + if result % 1 == 0: + return str(int(result)) + return str(result) + except (ValueError, TypeError): + # If can't convert to numbers, concatenate as strings + return str(left) + str(right) + + def subtract(self, args): + """Handle subtraction operation""" + left, right = args + left_val = float(left) if left != "None" and left is not None else 0.0 + right_val = float(right) if right != "None" and right is not None else 0.0 + result = left_val - right_val + if result % 1 == 0: + return str(int(result)) + return str(result) + + def multiply(self, args): + """Handle multiplication operation""" + left, right = args + left_val = float(left) if left != "None" and left is not None else 0.0 + right_val = float(right) if right != "None" and right is not None else 0.0 + result = left_val * right_val + if result % 1 == 0: + return str(int(result)) + return str(result) + + def divide(self, args): + """Handle division operation""" + left, right = args + left_val = float(left) if left != "None" and left is not None else 0.0 + right_val = float(right) if right != "None" and right is not None else 1.0 + if right_val == 0: + return "inf" # or raise an error, or return "0" + result = left_val / right_val + if result % 1 == 0: + return str(int(result)) + return str(result) + def function(self, args): return args[0] @@ -211,7 +296,7 @@ class FormatTransformer(lark.Transformer): return str(args[0]).title() def concat(self, args): - return "".join(args) + return "".join(str(arg) for arg in args) def substr(self, args): if len(args) == 3: @@ -241,13 +326,14 @@ class FormatTransformer(lark.Transformer): return str(result) def number(self, args): - if isinstance(args[0], str): - args[0] = float(args[0]) if "." in args[0] else int(args[0]) + arg_val = args[0] + if isinstance(arg_val, str): + arg_val = float(arg_val) if "." in arg_val else int(arg_val) if len(args) >= 3 and args[2]: - return "{:,}".format(args[0]).replace(".", "*").replace(",", args[2]).replace("*", args[1]) + return "{:,}".format(arg_val).replace(".", "*").replace(",", args[2]).replace("*", args[1]) elif len(args) >= 2 and args[1]: - return "{}".format(args[0]).replace(".", args[1]) - return "{:,}".format(args[0]) + return "{}".format(arg_val).replace(".", args[1]) + return "{:,}".format(arg_val) def format_length(self, args): return args[0] @@ -313,8 +399,18 @@ class GetElementTransformer(lark.Transformer): return args[1:-1].replace("\\", "") -def format(query: str) -> str: - return FormatTransformer().transform(format_grammar.parse(query)) +def format(query: str, element: Optional[ifcopenshell.entity_instance] = None) -> str: + """Format a query string with optional element context for variable substitution. + + :param query: Format query string (can include {{variable}} placeholders) + :param element: Optional IFC element for variable substitution + :return: Formatted string + + Example: + format("{{z}} / 2", element) # Substitutes element's z value + format("imperial_length({{z}} / 2, 4)", element) # Uses z in calculation + """ + return FormatTransformer(element).transform(format_grammar.parse(query)) def get_element_value(element: ifcopenshell.entity_instance, query: str) -> Any: @@ -1163,4 +1259,4 @@ class FacetTransformer(lark.Transformer): if comparison.startswith("!"): return not result - return result + return result \ No newline at end of file