Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 119 additions & 23 deletions src/ifcopenshell-python/ifcopenshell/util/selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,21 +139,32 @@
)

format_grammar = lark.Lark(
"""start: function

function: round | number | int | format_length | lower | upper | title | concat | substr | ESCAPED_STRING | NUMBER

round: "round(" function "," NUMBER ")"
number: "number(" function ["," ESCAPED_STRING ["," ESCAPED_STRING]] ")"
int: "int(" function ")"
"""start: expression

?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 ")"

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"
Expand Down Expand Up @@ -189,9 +200,83 @@


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]

Expand All @@ -211,7 +296,7 @@ def title(self, args):
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:
Expand Down Expand Up @@ -241,13 +326,14 @@ def round(self, args):
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]
Expand Down Expand Up @@ -313,8 +399,18 @@ def ESCAPED_STRING(self, args):
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:
Expand Down Expand Up @@ -1163,4 +1259,4 @@ def compare(self, element_value, comparison, value) -> bool:

if comparison.startswith("!"):
return not result
return result
return result
Loading