diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst index cb4628d416..9242908fbb 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst @@ -56,7 +56,7 @@ Filtering is typically used to select any IFC element or type. "``IfcWall, Pset_WallCommon.FireRating=2HR``", "Any 2 hour fire rated wall" - "``IfcWall, Pset_WallCommon.ThermalTransmittance=""1.5""``", "Any wall with a U-value of 1.5. Note the quotes: ``1.5`` contains a ``.``, so unquoted it is a syntax error. See `Quoting values in filters`_." + "``IfcWall, Pset_WallCommon.ThermalTransmittance=1.5``", "Any wall with a U-value of 1.5. A decimal number is the one kind of value that may contain a ``.`` unquoted. See `Quoting values in filters`_." "``IfcWall, IfcColumn, IfcBeam, IfcFooting, /Pset_.*Common/.LoadBearing=TRUE``", "Any load bearing structure" @@ -164,27 +164,31 @@ contain any of the following characters: , . = > < * ! and whitespace -If yours contains one of them, quote it. Every one of them except ``,`` is a -syntax error when left unquoted. The ``,`` is the more dangerous case, because -it does not error: it is read as the separator between two filters. So -``Name=Foo,IfcWall`` does not look for the literal name ``Foo,IfcWall``, it -quietly means "named ``Foo`` **and** an ``IfcWall``". Write -``Name="Foo,IfcWall"`` to match the literal value. +There is one exception, and it is only for a ``{{value}}``: a value that is a +plain decimal number may contain the ``.`` unquoted, so +``ThermalTransmittance=1.5`` is fine. A ``{{pset}}``, ``{{prop}}``, or +``{{keys}}`` containing a ``.`` always needs quoting. -The ``.`` is the one most likely to catch you out. It separates a property set -from a property, so it cannot also appear in an unquoted value, and that makes -every decimal number a syntax error unless it is quoted: +Otherwise, if yours contains one of these characters, quote it. Every one of +them except ``,`` is a syntax error when left unquoted. The ``,`` is the more +dangerous case, because it does not error: it is read as the separator between +two filters. So ``Name=Foo,IfcWall`` does not look for the literal name +``Foo,IfcWall``, it quietly means "named ``Foo`` **and** an ``IfcWall``". +Write ``Name="Foo,IfcWall"`` to match the literal value. + +The ``.`` still separates a property set from a property, so outside of a +number it cannot appear in an unquoted value: .. code-block:: - Pset_WallCommon.ThermalTransmittance=1.5 # syntax error - Pset_WallCommon.ThermalTransmittance="1.5" # correct + Pset_WallCommon.ThermalTransmittance=1.5 # a number, no quotes needed + Pset_WallCommon.ThermalTransmittance>-.5 # signed and leading dot too + Name=v1.2 # syntax error, not a number + Name="v1.2" # correct -Whole numbers are unaffected, which is why ``FireRating=2HR`` and -``ThermalTransmittance>1`` are fine unquoted while ``ThermalTransmittance>1.5`` -is not. Quoting a number does not turn the check into a text comparison - -``>``, ``>=``, ``<``, and ``<=`` still compare numerically, so -``ThermalTransmittance>"0.9"`` does match a value of ``1.5``. +Quoting a number is still allowed and means exactly the same thing. Either way +the check is not a text comparison - ``>``, ``>=``, ``<``, and ``<=`` compare +numerically, so ``ThermalTransmittance>0.9`` does match a value of ``1.5``. .. note:: diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index aa52e76f2b..d3bfc98c48 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -66,8 +66,10 @@ filter_elements_grammar = lark.Lark("""start: filter_group attribute_name: /[A-Z]\\w+/ ifc_class: /Ifc\\w+/ - value: special | quoted_string | regex_string | unquoted_string + value: special | quoted_string | regex_string | decimal_string | unquoted_string unquoted_string: /[^,.=><*!\\s]+/ + decimal_string: SIGNED_DECIMAL + SIGNED_DECIMAL: ["+"|"-"] (INT "." INT | "." INT) regex_string: "/" /[^\\/]+/ "/" quoted_string: ESCAPED_STRING @@ -1233,6 +1235,8 @@ class FacetTransformer(lark.Transformer): def value(self, args): if args[0].data == "unquoted_string": return args[0].children[0].value + elif args[0].data == "decimal_string": + return args[0].children[0].value elif args[0].data == "quoted_string": return args[0].children[0].value[1:-1].replace('\\"', '"') elif args[0].data == "regex_string": diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index c4c7a6ce17..be914033f1 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import lark import numpy as np import pytest @@ -307,6 +308,8 @@ class TestFilterElements(test.bootstrap.IFC4): ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Baz": 123}) assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz=123") == {element} ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Bay": 123.3}) + assert subject.filter_elements(self.file, "IfcWall, Foobar.Bay=123.3") == {element} + assert subject.filter_elements(self.file, 'IfcWall, Foobar.Bay="123.3"') == {element} pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="Pset_WallCommon") ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Status": ["New"]}) assert subject.filter_elements(self.file, "IfcWall, Pset_WallCommon.Status=New") == {element} @@ -331,6 +334,35 @@ class TestFilterElements(test.bootstrap.IFC4): 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_property_with_an_unquoted_decimal(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="Foobar") + ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Baz": 1.5}) + # A decimal needs no quotes. The pset/prop separator has already been + # consumed by the comparison, so a "." is unambiguous in a value. + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz=1.5") == {element} + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz>1") == {element} + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz>1.4") == {element} + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz<1.4") == set() + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz>=1.5") == {element} + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz<=1.5") == {element} + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz!=1.5") == {element2} + # Signed and leading dot forms. + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz>-1.5") == {element} + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz>+1.4") == {element} + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz>.5") == {element} + # Quoting a decimal stays legal and means exactly the same thing. + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz=1.5") == subject.filter_elements( + self.file, 'IfcWall, Foobar.Baz="1.5"' + ) + assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz>1.4") == subject.filter_elements( + self.file, 'IfcWall, Foobar.Baz>"1.4"' + ) + # A dot in a value is still only a number. Anything else needs quotes. + with pytest.raises(lark.exceptions.UnexpectedInput): + subject.filter_elements(self.file, "IfcWall, Foobar.Baz=v1.2") + def test_selecting_by_classification(self): project = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")