mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-21 14:23:53 +00:00
Accept an unquoted decimal in a selector value
Requiring quotes around a decimal was a surprise with no way to predict it: Foobar.Baz>1 works, Foobar.Baz>1.5 is a syntax error, and > >= < <= are almost always used on numbers. Writing Width>"0.2" also reads like a text comparison even though it is not. The "." is excluded from unquoted_string because it separates a pset from a property, but that separator has already been consumed by the time a value is read, so a number is unambiguous in value position. Add a decimal_string alternative to the value rule only. Signed and leading dot forms are accepted, 1. is not, and pset, prop and query keys are unchanged - they still need quoting for a ".". The transformer returns the same string a quoted value would, so =1.5 and ="1.5" are the same query and compare() does the existing type coercion. Nothing that parses today changes meaning; only inputs that used to be a syntax error now work. Update the quoting docs accordingly, and assert the Bay property that the filter test set up but never checked. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -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::
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user