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:
Bruno Postle
2026-09-09 20:30:37 +01:00
committed by Dion Moult
parent 1c6362ec31
commit 41390dad92
3 changed files with 58 additions and 18 deletions
@@ -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")