diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 7d2c3b5869..1dcb9a0ba0 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -291,9 +291,10 @@ def filter_elements( :type ifc_file: ifcopenshell.file :param query: Query to execute :type query: str - :param elements: Base set of IFC elements for the query. - If provided, new elements found for the current query will be added to `elements`. - Elements explicitly excluded in the `query` will also be excluded from `elements` + :param elements: Base set of IFC elements for the query. If not provided, + all elements in the IFC are queried. If provided, the query will be + applied to this set of elements, so the result will be a subset of + elements. :type elements: set[ifcopenshell.entity_instance], optional :param edit_in_place: If `True`, mutate the provided `elements` in place. Defaults to `False` :type edit_in_place: bool @@ -497,11 +498,17 @@ def set_element_value( f"Failed to set value for element '{original_element}' with query '{query}' (invalid or unsupported query)." ) + class FacetTransformer(lark.Transformer): def __init__(self, ifc_file: ifcopenshell.file, elements: Optional[set[ifcopenshell.entity_instance]] = None): self.file = ifc_file self.results = [] - self.elements = set() if elements is None else elements + if elements is None: + self.base_elements = None + self.elements = set() + else: + self.base_elements = elements.copy() + self.elements = set() self.container_parents = {} self.container_trees = {} @@ -517,28 +524,44 @@ class FacetTransformer(lark.Transformer): self.elements = set() def instance(self, args): - if args[0].data == "globalid": - try: - self.elements.add(self.file.by_guid(args[0].children[0].value)) - except: - pass + if self.base_elements is None: + if args[0].data == "globalid": + try: + self.elements.add(self.file.by_guid(args[0].children[0].value)) + except: + pass + else: + try: + self.elements.remove(self.file.by_guid(args[1].children[0].value)) + except: + pass else: - try: - self.elements.remove(self.file.by_guid(args[1].children[0].value)) - except: - pass + if args[0].data == "globalid": + self.elements |= { + e for e in self.base_elements if getattr(e, "GlobalId", None) == args[0].children[0].value + } + else: + self.elements -= { + e for e in self.base_elements if getattr(e, "GlobalId", None) == args[1].children[0].value + } def entity(self, args): - if args[0].data == "ifc_class": - try: - self.elements |= set(self.file.by_type(args[0].children[0].value)) - except: - pass + if self.base_elements is None: + if args[0].data == "ifc_class": + try: + self.elements |= set(self.file.by_type(args[0].children[0].value)) + except: + pass + else: + try: + self.elements -= set(self.file.by_type(args[1].children[0].value)) + except: + pass else: - try: - self.elements -= set(self.file.by_type(args[1].children[0].value)) - except: - pass + if args[0].data == "ifc_class": + self.elements |= {e for e in self.base_elements if e.is_a(args[0].children[0].value)} + else: + self.elements -= {e for e in self.base_elements if e.is_a(args[1].children[0].value)} def attribute(self, args): name, comparison, value = args diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 37a103d313..efcc5dfa9c 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -25,41 +25,41 @@ import ifcopenshell.util.pset import numpy as np -class TestFormat(): +class TestFormat: def test_no_formatting(self): assert subject.format("123") == "123" - assert subject.format('\"123\"') == "123" - assert subject.format('\"foo\"') == "foo" + assert subject.format('"123"') == "123" + assert subject.format('"foo"') == "foo" def test_string_formatting(self): - assert subject.format('upper(\"fOo\")') == "FOO" - assert subject.format('lower(\"fOo\")') == "foo" - assert subject.format('title(\"fOo\")') == "Foo" - assert subject.format('concat(\"fOo\", \"bar\")') == "fOobar" - assert subject.format('upper(concat(\"fOo\", \"bar\"))') == "FOOBAR" - assert subject.format('substr(\"foobar\", 3)') == "bar" - assert subject.format('substr(\"foobar\", 1, 2)') == "o" - assert subject.format('substr(\"foobar\", 1, -1)') == "ooba" + assert subject.format('upper("fOo")') == "FOO" + assert subject.format('lower("fOo")') == "foo" + assert subject.format('title("fOo")') == "Foo" + assert subject.format('concat("fOo", "bar")') == "fOobar" + assert subject.format('upper(concat("fOo", "bar"))') == "FOOBAR" + assert subject.format('substr("foobar", 3)') == "bar" + assert subject.format('substr("foobar", 1, 2)') == "o" + assert subject.format('substr("foobar", 1, -1)') == "ooba" def test_number_formatting(self): assert subject.format("round(123, 5)") == "125" - assert subject.format('round(\"123\", 5)') == "125" - assert subject.format('number(123)') == "123" - assert subject.format('number(1234.56)') == "1,234.56" + assert subject.format('round("123", 5)') == "125" + assert subject.format("number(123)") == "123" + assert subject.format("number(1234.56)") == "1,234.56" assert subject.format('number(123, ".")') == "123" - assert subject.format('number(\"123\", ".")') == "123" + assert subject.format('number("123", ".")') == "123" assert subject.format('number(123.12, ".")') == "123.12" assert subject.format('number(123.12, ",")') == "123,12" assert subject.format('number(1234.12, ",", ".")') == "1.234,12" - assert subject.format('metric_length(123, 5, 2)') == "125.00" - assert subject.format('metric_length(123.123, 0.1, 2)') == "123.10" - assert subject.format('metric_length(\"123\", 5, 2)') == "125.00" - assert subject.format('imperial_length(1, 1)') == "1'" - assert subject.format('imperial_length(3.123, 1)') == "3' - 1\"" - assert subject.format('imperial_length(3.123, 2)') == "3' - 1 1/2\"" - assert subject.format('imperial_length(\"3.123\", 2)') == "3' - 1 1/2\"" - assert subject.format('imperial_length(\"123.123\", 2, \"inch\", \"foot\")') == "10' - 3\"" - assert subject.format('imperial_length(\"123.123\", 2, \"inch\", \"inch\")') == "123\"" + assert subject.format("metric_length(123, 5, 2)") == "125.00" + assert subject.format("metric_length(123.123, 0.1, 2)") == "123.10" + assert subject.format('metric_length("123", 5, 2)') == "125.00" + assert subject.format("imperial_length(1, 1)") == "1'" + assert subject.format("imperial_length(3.123, 1)") == "3' - 1\"" + assert subject.format("imperial_length(3.123, 2)") == "3' - 1 1/2\"" + assert subject.format('imperial_length("3.123", 2)') == "3' - 1 1/2\"" + assert subject.format('imperial_length("123.123", 2, "inch", "foot")') == "10' - 3\"" + assert subject.format('imperial_length("123.123", 2, "inch", "inch")') == '123"' class TestGetElementValue(test.bootstrap.IFC4): @@ -269,17 +269,19 @@ class TestFilterElements(test.bootstrap.IFC4): def test_using_elements_argument(self): wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") slab = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + door = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcDoor") + elements = {wall, slab} + results = subject.filter_elements(self.file, "IfcWall", {wall, slab}) + assert results != elements + assert results == {wall} + elements = {wall, slab, door} + assert subject.filter_elements(self.file, "IfcWall, IfcSlab", elements) == {wall, slab} - # keep elements unaffected by expression - assert subject.filter_elements(self.file, "IfcWall", {slab}) == {wall, slab} - - # filter out excluded elements - assert subject.filter_elements(self.file, "IfcWall, ! IfcSlab", {slab}) == {wall} - - # edit_in_place to update original set - original_set = set() + def test_editing_in_place(self): + wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + original_set = {wall} new_set = subject.filter_elements(self.file, "IfcWall", original_set, edit_in_place=True) - assert new_set == original_set + assert new_set == original_set == {wall} class TestSetElementValue(test.bootstrap.IFC4):