mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-24 02:08:05 +00:00
Reimplement sort / reverse / join function to format language, simplify text annotation variables, add tests
Previously, sort, reverse list, and join functionality was implemented as special cases in Bonsai itself. Given that it has usecases (especially in material lists, but any sort of list applies) I've moved this function into the IOS formatting language. The IOS formatting language previously wasn't capable of this, but the awesome addition by @falken10vdl made the formatting language accept queries inline, so that means it can handle lists. I also added tests for all the new functions and expression syntax (+-*/ operators). I simplified the code that gets the evaluated text literal - previously it seems to call format() multiple times.
This commit is contained in:
@@ -365,15 +365,10 @@ class DecoratorData:
|
|||||||
|
|
||||||
for literal in literals:
|
for literal in literals:
|
||||||
literal_value = literal.Literal
|
literal_value = literal.Literal
|
||||||
try:
|
|
||||||
eval_value = cls.evaluate_formatting_expressions(literal_value, product)
|
|
||||||
current_value = tool.Drawing.replace_text_literal_variables(eval_value, product)
|
|
||||||
except Exception:
|
|
||||||
current_value = literal_value
|
|
||||||
literal_data = {
|
literal_data = {
|
||||||
"Literal": literal_value,
|
"Literal": literal_value,
|
||||||
"BoxAlignment": literal.BoxAlignment,
|
"BoxAlignment": literal.BoxAlignment,
|
||||||
"CurrentValue": current_value,
|
"CurrentValue": tool.Drawing.replace_text_literal_variables(literal_value, product),
|
||||||
}
|
}
|
||||||
literals_data.append(literal_data)
|
literals_data.append(literal_data)
|
||||||
|
|
||||||
@@ -399,21 +394,6 @@ class DecoratorData:
|
|||||||
|
|
||||||
return element
|
return element
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def evaluate_formatting_expressions(cls, text: str, element=None) -> str:
|
|
||||||
"""Evaluate formatting expressions wrapped in backticks using ifcopenshell.util.selector.format, always passing element context"""
|
|
||||||
import re
|
|
||||||
|
|
||||||
def evaluate_expression(match):
|
|
||||||
try:
|
|
||||||
expression = match.group(1)
|
|
||||||
result = ifcopenshell.util.selector.format(expression, element)
|
|
||||||
return str(result)
|
|
||||||
except Exception as e:
|
|
||||||
return match.group(0)
|
|
||||||
|
|
||||||
return re.sub(r"``([^`]+)``", evaluate_expression, text)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_element_value_by_key(cls, element: ifcopenshell.entity_instance, key: str):
|
def get_element_value_by_key(cls, element: ifcopenshell.entity_instance, key: str):
|
||||||
"""Get element value by its key using IfcOpenShell selector syntax"""
|
"""Get element value by its key using IfcOpenShell selector syntax"""
|
||||||
|
|||||||
@@ -1306,9 +1306,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
assert element
|
assert element
|
||||||
# updating text font size in EPset_Annotation.Classes
|
# updating text font size in EPset_Annotation.Classes
|
||||||
print("we got", font_size, repr(font_size))
|
|
||||||
font_size_str = next((key for key in FONT_SIZES if FONT_SIZES[key] == font_size), None)
|
font_size_str = next((key for key in FONT_SIZES if FONT_SIZES[key] == font_size), None)
|
||||||
print("so", font_size_str)
|
|
||||||
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
|
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
|
||||||
assert isinstance(classes, Union[str, None])
|
assert isinstance(classes, Union[str, None])
|
||||||
classes_split = classes.split() if classes else []
|
classes_split = classes.split() if classes else []
|
||||||
@@ -2101,16 +2099,13 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
if not product:
|
if not product:
|
||||||
return text
|
return text
|
||||||
|
|
||||||
for command in re.findall("``.*?``", text):
|
for command in re.findall("``.+?``", text):
|
||||||
original_command = command
|
original_command = command
|
||||||
command_content = command[2:-2]
|
command_content = command[2:-2]
|
||||||
if command_content is None or str(command_content).strip().lower() == "none":
|
try:
|
||||||
|
text = text.replace(original_command, ifcopenshell.util.selector.format(command_content, product))
|
||||||
|
except Exception:
|
||||||
text = text.replace(original_command, "")
|
text = text.replace(original_command, "")
|
||||||
else:
|
|
||||||
try:
|
|
||||||
text = text.replace(original_command, ifcopenshell.util.selector.format(command_content, product))
|
|
||||||
except Exception:
|
|
||||||
text = text.replace(original_command, "")
|
|
||||||
for variable in re.findall("{{.*?}}", text):
|
for variable in re.findall("{{.*?}}", text):
|
||||||
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
|
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
|
||||||
if isinstance(value, (list, tuple)):
|
if isinstance(value, (list, tuple)):
|
||||||
|
|||||||
@@ -252,6 +252,10 @@ nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce
|
|||||||
"``number({{value}}[, {{decimal_separator}}[, {{thousands_separator}}]])``", "``number(1234.56, "","", ""."")``", "``1.234,56``", "Formats {{value}} with an optional custom {{decimal_separator}} and {{thousands_separator}}. The default separators are ``.`` and ``,``."
|
"``number({{value}}[, {{decimal_separator}}[, {{thousands_separator}}]])``", "``number(1234.56, "","", ""."")``", "``1.234,56``", "Formats {{value}} with an optional custom {{decimal_separator}} and {{thousands_separator}}. The default separators are ``.`` and ``,``."
|
||||||
"``metric_length({{value}}, {{precision}}, {{decimals}})``", "``metric_length(3.123, 0.1, 2)``", "``3.10``", "Rounds ``{{value}}`` to the nearest ``{{precision}}`` then displays using a certain amount of decimal places."
|
"``metric_length({{value}}, {{precision}}, {{decimals}})``", "``metric_length(3.123, 0.1, 2)``", "``3.10``", "Rounds ``{{value}}`` to the nearest ``{{precision}}`` then displays using a certain amount of decimal places."
|
||||||
"``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}}, {{suppress_zero_inches}})``", "``imperial_length(3.0, 4, ""foot"", ""foot"", true)`` OR ``imperial_length(3.0, 4, ""foot"", ""foot"", false)``", "``3'`` OR ``3' - 0""``", "The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is then rounded to the nearest ``1/{{precision}}`` inch, then formatted using fractional feet and inches if ``{{output_unit}}`` is set to ``foot``, or just inches if ``{{output_unit}}`` is set to ``inch``. When ``{{suppress_zero_inches}}`` is ``true`` (default), measurements with zero inches will omit the inch portion (e.g., ``3'`` instead of ``3' - 0""``)."
|
"``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}}, {{suppress_zero_inches}})``", "``imperial_length(3.0, 4, ""foot"", ""foot"", true)`` OR ``imperial_length(3.0, 4, ""foot"", ""foot"", false)``", "``3'`` OR ``3' - 0""``", "The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is then rounded to the nearest ``1/{{precision}}`` inch, then formatted using fractional feet and inches if ``{{output_unit}}`` is set to ``foot``, or just inches if ``{{output_unit}}`` is set to ``inch``. When ``{{suppress_zero_inches}}`` is ``true`` (default), measurements with zero inches will omit the inch portion (e.g., ``3'`` instead of ``3' - 0""``)."
|
||||||
|
"``sort({{values}})``", "``sort({{mats.Name}})``", "``Name1, Name2``", "Sorts a list of items."
|
||||||
|
"``reverse({{values}})``", "``reverse({{mats.Name}})``", "``Name2, Name1``", "Reverses a list of items."
|
||||||
|
"``join({{separator}}, {{values}})``", "``join("-", {{mats.Name}})``", "``Name1-Name2``", "Joins a list of items with a custom separator. By default, all lists a rendered as comma separated."
|
||||||
|
"``{{value1}}[+-*/]{{value2}}``", "``{{z}}+3``", "``5``", "Does arithmetic. Typical operators such as +, -, \*, and / are allowed and can be mixed with other variables and formatting functions."
|
||||||
|
|
||||||
When using queries in an IfcAnnotation tag surround with backticks.
|
When using queries in an IfcAnnotation tag surround with backticks.
|
||||||
Examples:
|
Examples:
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ format_grammar = lark.Lark(
|
|||||||
| mul_div "*" function -> multiply
|
| mul_div "*" function -> multiply
|
||||||
| mul_div "/" function -> divide
|
| mul_div "/" function -> divide
|
||||||
|
|
||||||
function: round | number | int | format_length | lower | upper | title | concat | substr | variable | ESCAPED_STRING | SIGNED_NUMBER | "(" expression ")"
|
function: round | number | int | format_length | lower | upper | title | concat | substr | sort | reverse | join | variable | ESCAPED_STRING | SIGNED_NUMBER | "(" expression ")"
|
||||||
|
|
||||||
variable: "{{" query_path "}}"
|
variable: "{{" query_path "}}"
|
||||||
query_path: /[^}]+/
|
query_path: /[^}]+/
|
||||||
@@ -160,6 +160,9 @@ format_grammar = lark.Lark(
|
|||||||
title: "title(" expression ")"
|
title: "title(" expression ")"
|
||||||
concat: "concat(" expression ("," expression)* ")"
|
concat: "concat(" expression ("," expression)* ")"
|
||||||
substr: "substr(" expression "," SIGNED_INT ["," SIGNED_INT] ")"
|
substr: "substr(" expression "," SIGNED_INT ["," SIGNED_INT] ")"
|
||||||
|
sort: "sort(" expression ")"
|
||||||
|
reverse: "reverse(" expression ")"
|
||||||
|
join: "join(" ESCAPED_STRING "," expression ")"
|
||||||
boolean: TRUE | FALSE
|
boolean: TRUE | FALSE
|
||||||
|
|
||||||
TRUE: "true" | "True" | "TRUE"
|
TRUE: "true" | "True" | "TRUE"
|
||||||
@@ -201,6 +204,8 @@ class FormatTransformer(lark.Transformer):
|
|||||||
self.element = element
|
self.element = element
|
||||||
|
|
||||||
def start(self, args):
|
def start(self, args):
|
||||||
|
if isinstance(args[0], (list, tuple)):
|
||||||
|
return ", ".join(args[0])
|
||||||
return args[0]
|
return args[0]
|
||||||
|
|
||||||
def expression(self, args):
|
def expression(self, args):
|
||||||
@@ -208,18 +213,11 @@ class FormatTransformer(lark.Transformer):
|
|||||||
|
|
||||||
def variable(self, args):
|
def variable(self, args):
|
||||||
"""Handle variable substitution like {{z}} or {{Pset_Wall.FireRating}}"""
|
"""Handle variable substitution like {{z}} or {{Pset_Wall.FireRating}}"""
|
||||||
if self.element is None:
|
if self.element:
|
||||||
return "0" # Default value if no element context
|
try:
|
||||||
|
return get_element_value(self.element, args[0])
|
||||||
query_path = args[0]
|
except:
|
||||||
try:
|
pass
|
||||||
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):
|
def query_path(self, args):
|
||||||
"""Extract the query path from variable"""
|
"""Extract the query path from variable"""
|
||||||
@@ -301,6 +299,15 @@ class FormatTransformer(lark.Transformer):
|
|||||||
elif len(args) == 2:
|
elif len(args) == 2:
|
||||||
return str(args[0])[int(args[1]) :]
|
return str(args[0])[int(args[1]) :]
|
||||||
|
|
||||||
|
def sort(self, args):
|
||||||
|
return sorted(args[0])
|
||||||
|
|
||||||
|
def reverse(self, args):
|
||||||
|
return list(reversed(args[0]))
|
||||||
|
|
||||||
|
def join(self, args):
|
||||||
|
return args[0].join(args[1])
|
||||||
|
|
||||||
def boolean(self, args):
|
def boolean(self, args):
|
||||||
if not args:
|
if not args:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import ifcopenshell.util.selector as subject
|
|||||||
import test.bootstrap
|
import test.bootstrap
|
||||||
|
|
||||||
|
|
||||||
class TestFormat:
|
class TestFormat(test.bootstrap.IFC4):
|
||||||
def test_no_formatting(self):
|
def test_no_formatting(self):
|
||||||
assert subject.format("123") == "123"
|
assert subject.format("123") == "123"
|
||||||
assert subject.format('"123"') == "123"
|
assert subject.format('"123"') == "123"
|
||||||
@@ -78,6 +78,38 @@ class TestFormat:
|
|||||||
assert subject.format('imperial_length(3.0, 4, "foot", "foot", false)') == "3' - 0\""
|
assert subject.format('imperial_length(3.0, 4, "foot", "foot", false)') == "3' - 0\""
|
||||||
assert subject.format('imperial_length(3.0, 4, "foot", "foot", False)') == "3' - 0\""
|
assert subject.format('imperial_length(3.0, 4, "foot", "foot", False)') == "3' - 0\""
|
||||||
|
|
||||||
|
def test_variable_formatting(self):
|
||||||
|
assert subject.format('{{undefined}}') is None
|
||||||
|
assert subject.format('upper({{undefined}})') == "NONE"
|
||||||
|
assert subject.format('int({{undefined}})') == "0"
|
||||||
|
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||||
|
assert subject.format('{{undefined}}', element) is None
|
||||||
|
assert subject.format('{{class}}', element) == "IfcWall"
|
||||||
|
assert subject.format('{{ class }}', element) == "IfcWall"
|
||||||
|
assert subject.format('upper({{ class }})', element) == "IFCWALL"
|
||||||
|
|
||||||
|
def test_list_formatting(self):
|
||||||
|
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||||
|
material = ifcopenshell.api.material.add_material(self.file, name="CON01")
|
||||||
|
material2 = ifcopenshell.api.material.add_material(self.file, name="CON03")
|
||||||
|
material3 = ifcopenshell.api.material.add_material(self.file, name="CON02")
|
||||||
|
material_set = ifcopenshell.api.material.add_material_set(self.file, set_type="IfcMaterialLayerSet")
|
||||||
|
layer = ifcopenshell.api.material.add_layer(self.file, layer_set=material_set, material=material)
|
||||||
|
layer = ifcopenshell.api.material.add_layer(self.file, layer_set=material_set, material=material2)
|
||||||
|
layer = ifcopenshell.api.material.add_layer(self.file, layer_set=material_set, material=material3)
|
||||||
|
ifcopenshell.api.material.assign_material(self.file, products=[element], material=material_set)
|
||||||
|
assert subject.format('{{materials.Name}}', element) == "CON01, CON03, CON02"
|
||||||
|
assert subject.format('sort({{materials.Name}})', element) == "CON01, CON02, CON03"
|
||||||
|
assert subject.format('reverse({{materials.Name}})', element) == "CON02, CON03, CON01"
|
||||||
|
assert subject.format('join("-", {{materials.Name}})', element) == "CON01-CON03-CON02"
|
||||||
|
|
||||||
|
def test_expressions(self):
|
||||||
|
assert subject.format('2+3') == "5"
|
||||||
|
assert subject.format('-2+3') == "1"
|
||||||
|
assert subject.format('2-3') == "-1"
|
||||||
|
assert subject.format('3*2') == "6"
|
||||||
|
assert subject.format('3/2') == "1.5"
|
||||||
|
|
||||||
|
|
||||||
class TestGetElementValue(test.bootstrap.IFC4):
|
class TestGetElementValue(test.bootstrap.IFC4):
|
||||||
def test_selecting_an_elements_class_or_id_using_a_query(self):
|
def test_selecting_an_elements_class_or_id_using_a_query(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user