From ca8e0146472401359a0ff29259ff1e2ea78dcaaf Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 13 Jan 2024 15:58:57 +1100 Subject: [PATCH] Fix #4195. Fix regressions from f840da3 and add support for "contains string" comparisons. --- .../ifcopenshell/util/selector.py | 96 +++++++++++-------- .../test/util/test_selector.py | 14 +++ 2 files changed, 69 insertions(+), 41 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 73f6bcf4b2..fc899f5a5d 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -55,19 +55,20 @@ filter_elements_grammar = lark.Lark( ifc_class: /Ifc\\w+/ value: special | quoted_string | regex_string | unquoted_string - unquoted_string: /[^,.=\\s]+/ + unquoted_string: /[^,.=><*!\\s]+/ regex_string: "/" /[^\\/]+/ "/" quoted_string: ESCAPED_STRING special: null | true | false - comparison: not? equals | morethanequalto | lessthanequalto | morethan | lessthan + comparison: not? equals | morethanequalto | lessthanequalto | morethan | lessthan | not? contains not: "!" equals: "=" morethanequalto: ">=" lessthanequalto: "<=" morethan: ">" lessthan: "<" + contains: "*=" null: "NULL" true: "TRUE" false: "FALSE" @@ -557,18 +558,24 @@ class FacetTransformer(lark.Transformer): return tree def comparison(self, args): - if args[0].data == "equals": - return "=" - elif args[0].data == "morethanequalto": - return ">=" - elif args[0].data == "lessthanequalto": - return "<=" - elif args[0].data == "morethan": - return ">" - elif args[0].data == "lessthan": - return "<" + if args[0].data == "not": + comparison = args[1].data + is_not = "!" else: - return "!=" + comparison = args[0].data + is_not = "" + + return ( + is_not + + { + "equals": "=", + "morethanequalto": ">=", + "lessthanequalto": "<=", + "morethan": ">", + "lessthan": "<", + "contains": "*=", + }[comparison] + ) def keys(self, args): return self.value(args) @@ -595,35 +602,42 @@ class FacetTransformer(lark.Transformer): return False def compare(self, element_value, comparison, value): - if element_value: - if isinstance(element_value, (list, tuple)): - return any(self.compare(ev, comparison, value) for ev in element_value) - elif isinstance(value, re.Pattern): - result = bool(value.match(element_value)) if element_value is not None else False - elif isinstance(value, str): - if isinstance(element_value, int): - value = int(value) - elif isinstance(element_value, float): - value = float(value) - - if comparison == "=": - result = element_value == value - elif comparison == ">": - result = element_value > value - elif comparison == "<": - result = element_value < value - elif comparison == ">=": - result = element_value >= value - elif comparison == "<= ": - result = element_value <= value - else: - result = element_value != value + if isinstance(element_value, (list, tuple)): + return any(self.compare(ev, comparison, value) for ev in element_value) + elif isinstance(value, str): + if isinstance(element_value, int): + value = int(value) + elif isinstance(element_value, float): + value = float(value) - elif value in (None, True, False): - result = element_value is value - else: - result = False - return result + if isinstance(element_value, (int, float)): + operator = comparison.lstrip("!") + if operator == ">=": + result = element_value >= value + elif operator == "<=": + result = element_value <= value + elif operator == ">": + result = element_value > value + elif operator == "<": + result = element_value < value + else: + result = element_value == value # Tolerance? + elif isinstance(element_value, str): + operator = comparison.lstrip("!") + if operator == "*=": + result = value in element_value + else: + result = element_value == value + else: + result = element_value == value + elif isinstance(value, re.Pattern): + result = bool(value.match(element_value)) if element_value is not None else False + elif value in (None, True, False): + result = element_value is value + + if comparison.startswith("!"): + return not result + return result class Selector: diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 982f3a5095..b02ab7e811 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -192,6 +192,20 @@ class TestFilterElements(test.bootstrap.IFC4): ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Status": ["New"]}) assert subject.filter_elements(self.file, "IfcWall, Pset_WallCommon.Status=New") == {element} + def test_selecting_by_property_with_comparisons(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foobar") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Baz": 123}) + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz>100") == {element} + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz<100") == set() + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz>=100") == {element} + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz<=100") == set() + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Bar"}) + assert subject.filter_elements(self.file, "IfcWall, Foobar.Foo*=ar") == {element} + assert subject.filter_elements(self.file, "IfcWall, Foobar.Foo!*=ar") == {element2} + assert subject.filter_elements(self.file, "IfcWall, Foobar.Foo*=Foo") == set() + def test_selecting_by_classification(self): project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")