mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-18 06:21:40 +00:00
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)
This commit is contained in:
@@ -139,21 +139,32 @@ get_element_grammar = lark.Lark(
|
|||||||
)
|
)
|
||||||
|
|
||||||
format_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 ")"
|
variable: "{{" query_path "}}"
|
||||||
number: "number(" function ["," ESCAPED_STRING ["," ESCAPED_STRING]] ")"
|
query_path: /[^}]+/
|
||||||
int: "int(" function ")"
|
|
||||||
|
round: "round(" expression "," NUMBER ")"
|
||||||
|
number: "number(" expression ["," ESCAPED_STRING ["," ESCAPED_STRING]] ")"
|
||||||
|
int: "int(" expression ")"
|
||||||
format_length: metric_length | imperial_length
|
format_length: metric_length | imperial_length
|
||||||
metric_length: "metric_length(" function "," NUMBER "," NUMBER ")"
|
metric_length: "metric_length(" expression "," NUMBER "," NUMBER ")"
|
||||||
imperial_length: "imperial_length(" function "," NUMBER ["," ESCAPED_STRING "," ESCAPED_STRING ["," boolean]] ")"
|
imperial_length: "imperial_length(" expression "," NUMBER ["," ESCAPED_STRING "," ESCAPED_STRING ["," boolean]] ")"
|
||||||
lower: "lower(" function ")"
|
lower: "lower(" expression ")"
|
||||||
upper: "upper(" function ")"
|
upper: "upper(" expression ")"
|
||||||
title: "title(" function ")"
|
title: "title(" expression ")"
|
||||||
concat: "concat(" function ("," function)* ")"
|
concat: "concat(" expression ("," expression)* ")"
|
||||||
substr: "substr(" function "," SIGNED_INT ["," SIGNED_INT] ")"
|
substr: "substr(" expression "," SIGNED_INT ["," SIGNED_INT] ")"
|
||||||
boolean: TRUE | FALSE
|
boolean: TRUE | FALSE
|
||||||
|
|
||||||
TRUE: "true" | "True" | "TRUE"
|
TRUE: "true" | "True" | "TRUE"
|
||||||
@@ -189,9 +200,83 @@ format_grammar = lark.Lark(
|
|||||||
|
|
||||||
|
|
||||||
class FormatTransformer(lark.Transformer):
|
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):
|
def start(self, args):
|
||||||
return args[0]
|
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):
|
def function(self, args):
|
||||||
return args[0]
|
return args[0]
|
||||||
|
|
||||||
@@ -211,7 +296,7 @@ class FormatTransformer(lark.Transformer):
|
|||||||
return str(args[0]).title()
|
return str(args[0]).title()
|
||||||
|
|
||||||
def concat(self, args):
|
def concat(self, args):
|
||||||
return "".join(args)
|
return "".join(str(arg) for arg in args)
|
||||||
|
|
||||||
def substr(self, args):
|
def substr(self, args):
|
||||||
if len(args) == 3:
|
if len(args) == 3:
|
||||||
@@ -241,13 +326,14 @@ class FormatTransformer(lark.Transformer):
|
|||||||
return str(result)
|
return str(result)
|
||||||
|
|
||||||
def number(self, args):
|
def number(self, args):
|
||||||
if isinstance(args[0], str):
|
arg_val = args[0]
|
||||||
args[0] = float(args[0]) if "." in args[0] else int(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]:
|
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]:
|
elif len(args) >= 2 and args[1]:
|
||||||
return "{}".format(args[0]).replace(".", args[1])
|
return "{}".format(arg_val).replace(".", args[1])
|
||||||
return "{:,}".format(args[0])
|
return "{:,}".format(arg_val)
|
||||||
|
|
||||||
def format_length(self, args):
|
def format_length(self, args):
|
||||||
return args[0]
|
return args[0]
|
||||||
@@ -313,8 +399,18 @@ class GetElementTransformer(lark.Transformer):
|
|||||||
return args[1:-1].replace("\\", "")
|
return args[1:-1].replace("\\", "")
|
||||||
|
|
||||||
|
|
||||||
def format(query: str) -> str:
|
def format(query: str, element: Optional[ifcopenshell.entity_instance] = None) -> str:
|
||||||
return FormatTransformer().transform(format_grammar.parse(query))
|
"""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:
|
def get_element_value(element: ifcopenshell.entity_instance, query: str) -> Any:
|
||||||
@@ -1163,4 +1259,4 @@ class FacetTransformer(lark.Transformer):
|
|||||||
|
|
||||||
if comparison.startswith("!"):
|
if comparison.startswith("!"):
|
||||||
return not result
|
return not result
|
||||||
return result
|
return result
|
||||||
Reference in New Issue
Block a user