From 5a3ed1408d3d6d6f294413da1e898fbd9f85ec21 Mon Sep 17 00:00:00 2001 From: Guichard Desrosiers Date: Mon, 4 May 2026 10:47:40 -0400 Subject: [PATCH] Fix float/double facet checks to use native comparison instead of compareTo Float|Double.compareTo define a total order where NaN > all values and -0.0 < +0.0, which violates XSD semantics. XSD requires NaN to be incomparable (failing all facets) and -0.0 to equal +0.0. - Replace f.compareTo()/d.compareTo() with native f >= and d >= (and corresponding >, <=, <) so that minInclusive, maxInclusive, minExclusive, and maxExclusive correctly handle NaN, +/-INF, and -0.0 for xs:float and xs:double types. - Add unit tests to Facets.tdml covering all four facets for xs:float and xs:double, exercising IEEE 754 special values: NaN, +INF, -INF, and -0.0. DAFFODIL-3072 --- .../runtime1/processors/RuntimeData.scala | 28 +- .../daffodil/section05/facets/Facets.tdml | 753 +++++++++++++++++- .../section05/facets/TestFacets.scala | 34 + 3 files changed, 805 insertions(+), 10 deletions(-) diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/RuntimeData.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/RuntimeData.scala index adf4c23b32..49b0d0b1a6 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/RuntimeData.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/RuntimeData.scala @@ -522,9 +522,12 @@ final class SimpleTypeRuntimeData( ): Boolean = { // we must handle float and double separately because diNode.dataValue // could be Inf/Nan, which cannot be converted to BigDecimal + // we must also use native float/double comparison, not compareTo. Float|Double.compareTo + // define +0.0f|d > -0.0f|d (total order) and NaN > all values, but XSD requires math + // equality (-0.0 == +0.0) and NaN incomparable (fails all facets). diNode.dataValue.getAnyRef match { - case f: JFloat => f.compareTo(minValue.floatValue) >= 0 - case d: JDouble => d.compareTo(minValue.doubleValue) >= 0 + case f: JFloat => f >= minValue.floatValue + case d: JDouble => d >= minValue.doubleValue case _ => diNode.dataValueAsBigDecimal.compareTo(minValue) >= 0 } } @@ -537,9 +540,12 @@ final class SimpleTypeRuntimeData( ): Boolean = { // we must handle float and double separately because diNode.dataValue // could be Inf/Nan, which cannot be converted to BigDecimal + // we must also use native float/double comparison, not compareTo. Float|Double.compareTo + // define +0.0f|d > -0.0f|d (total order) and NaN > all values, but XSD requires math + // equality (-0.0 == +0.0) and NaN incomparable (fails all facets). diNode.dataValue.getAnyRef match { - case f: JFloat => f.compareTo(minValue.floatValue) > 0 - case d: JDouble => d.compareTo(minValue.doubleValue) > 0 + case f: JFloat => f > minValue.floatValue + case d: JDouble => d > minValue.doubleValue case _ => diNode.dataValueAsBigDecimal.compareTo(minValue) > 0 } } @@ -552,9 +558,12 @@ final class SimpleTypeRuntimeData( ): Boolean = { // we must handle float and double separately because diNode.dataValue // could be Inf/Nan, which cannot be converted to BigDecimal + // we must also use native float/double comparison, not compareTo. Float|Double.compareTo + // define +0.0f|d > -0.0f|d (total order) and NaN > all values, but XSD requires math + // equality (-0.0 == +0.0) and NaN incomparable (fails all facets). diNode.dataValue.getAnyRef match { - case f: JFloat => f.compareTo(maxValue.floatValue) <= 0 - case d: JDouble => d.compareTo(maxValue.doubleValue) <= 0 + case f: JFloat => f <= maxValue.floatValue + case d: JDouble => d <= maxValue.doubleValue case _ => diNode.dataValueAsBigDecimal.compareTo(maxValue) <= 0 } } @@ -567,9 +576,12 @@ final class SimpleTypeRuntimeData( ): Boolean = { // we must handle float and double separately because diNode.dataValue // could be Inf/Nan, which cannot be converted to BigDecimal + // we must also use native float/double comparison, not compareTo. Float|Double.compareTo + // define +0.0f|d > -0.0f|d (total order) and NaN > all values, but XSD requires math + // equality (-0.0 == +0.0) and NaN incomparable (fails all facets). diNode.dataValue.getAnyRef match { - case f: JFloat => f.compareTo(maxValue.floatValue) < 0 - case d: JDouble => d.compareTo(maxValue.doubleValue) < 0 + case f: JFloat => f < maxValue.floatValue + case d: JDouble => d < maxValue.doubleValue case _ => diNode.dataValueAsBigDecimal.compareTo(maxValue) < 0 } } diff --git a/daffodil-test/src/test/resources/org/apache/daffodil/section05/facets/Facets.tdml b/daffodil-test/src/test/resources/org/apache/daffodil/section05/facets/Facets.tdml index 3faeeab31f..b046793873 100644 --- a/daffodil-test/src/test/resources/org/apache/daffodil/section05/facets/Facets.tdml +++ b/daffodil-test/src/test/resources/org/apache/daffodil/section05/facets/Facets.tdml @@ -20,7 +20,8 @@ xmlns:tdml="http://www.ibm.com/xmlns/dfdl/testData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dfdl="http://www.ogf.org/dfdl/dfdl-1.0/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ct="http://w3.ibm.com/xmlns/dfdl/ctInfoset" xmlns:ex="http://example.com" - defaultRoundTrip="true"> + defaultRoundTrip="true" + defaultIgnoreUnexpectedValidationErrors="false"> @@ -7016,5 +7017,753 @@ '.3' is not a valid value for 'integer' - + + + + + , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 7f c0 00 00 + + + + NaN + + + + + minInclusive + + + + + + + + + 7f 80 00 00 + + + + INF + + + + + + + + + ff 80 00 00 + + + + -INF + + + + + minInclusive + + + + + + + + + 80 00 00 00 + + + + -0.0 + + + + + + + + + 7f c0 00 00 + + + + NaN + + + + + maxInclusive + + + + + + + + + 7f 80 00 00 + + + + INF + + + + + maxInclusive + + + + + + + + + ff 80 00 00 + + + + -INF + + + + + + + + 80 00 00 00 + + + + -0.0 + + + + + + + + + + 7f c0 00 00 + + + + NaN + + + + + minExclusive + + + + + + + + + 7f 80 00 00 + + + + INF + + + + + + + + + ff 80 00 00 + + + + -INF + + + + + minExclusive + + + + + + + + + 80 00 00 00 + + + + -0.0 + + + + + minExclusive + + + + + + + + + 7f c0 00 00 + + + + NaN + + + + + maxExclusive + + + + + + + + + 7f 80 00 00 + + + + INF + + + + + maxExclusive + + + + + + + + + ff 80 00 00 + + + + -INF + + + + + + + + + 80 00 00 00 + + + + -0.0 + + + + + maxExclusive + + + + + + + + + 7f f8 00 00 00 00 00 00 + + + + NaN + + + + + minInclusive + + + + + + + + + 7f f0 00 00 00 00 00 00 + + + + INF + + + + + + + + + ff f0 00 00 00 00 00 00 + + + + -INF + + + + + minInclusive + + + + + + + + + 80 00 00 00 00 00 00 00 + + + + -0.0 + + + + + + + + + 7f f8 00 00 00 00 00 00 + + + + NaN + + + + + maxInclusive + + + + + + + + + 7f f0 00 00 00 00 00 00 + + + + INF + + + + + maxInclusive + + + + + + + + + ff f0 00 00 00 00 00 00 + + + + -INF + + + + + + + + 80 00 00 00 00 00 00 00 + + + + -0.0 + + + + + + + + + + 7f f8 00 00 00 00 00 00 + + + + NaN + + + + + minExclusive + + + + + + + + + 7f f0 00 00 00 00 00 00 + + + + INF + + + + + + + + + ff f0 00 00 00 00 00 00 + + + + -INF + + + + + minExclusive + + + + + + + + + 80 00 00 00 00 00 00 00 + + + + -0.0 + + + + + minExclusive + + + + + + + + + 7f f8 00 00 00 00 00 00 + + + + NaN + + + + + maxExclusive + + + + + + + + + 7f f0 00 00 00 00 00 00 + + + + INF + + + + + maxExclusive + + + + + + + + + ff f0 00 00 00 00 00 00 + + + + -INF + + + + + + + + + 80 00 00 00 00 00 00 00 + + + + -0.0 + + + + + maxExclusive + + + + + diff --git a/daffodil-test/src/test/scala/org/apache/daffodil/section05/facets/TestFacets.scala b/daffodil-test/src/test/scala/org/apache/daffodil/section05/facets/TestFacets.scala index 2b299e2b39..83e9ea7503 100644 --- a/daffodil-test/src/test/scala/org/apache/daffodil/section05/facets/TestFacets.scala +++ b/daffodil-test/src/test/scala/org/apache/daffodil/section05/facets/TestFacets.scala @@ -238,4 +238,38 @@ class TestFacetsValidate extends TdmlTests { @Test def fractionDigitsFailNotInt = test @Test def totalDigits09 = test @Test def totalDigits10 = test + + @Test def floatMinInclusiveNaNFail = test + @Test def floatMinInclusivePosINFPass = test + @Test def floatMinInclusiveNegINFFail = test + @Test def floatMinInclusiveNegativeZeroPass = test + @Test def floatMaxInclusiveNaNFail = test + @Test def floatMaxInclusivePosINFFail = test + @Test def floatMaxInclusiveNegINFPass = test + @Test def floatMaxInclusiveNegativeZeroPass = test + @Test def floatMinExclusiveNaNFail = test + @Test def floatMinExclusivePosINFPass = test + @Test def floatMinExclusiveNegINFFail = test + @Test def floatMinExclusiveNegativeZeroFail = test + @Test def floatMaxExclusiveNaNFail = test + @Test def floatMaxExclusivePosINFFail = test + @Test def floatMaxExclusiveNegINFPass = test + @Test def floatMaxExclusiveNegativeZeroFail = test + + @Test def doubleMinInclusiveNaNFail = test + @Test def doubleMinInclusivePosINFPass = test + @Test def doubleMinInclusiveNegINFFail = test + @Test def doubleMinInclusiveNegativeZeroPass = test + @Test def doubleMaxInclusiveNaNFail = test + @Test def doubleMaxInclusivePosINFFail = test + @Test def doubleMaxInclusiveNegINFPass = test + @Test def doubleMaxInclusiveNegativeZeroPass = test + @Test def doubleMinExclusiveNaNFail = test + @Test def doubleMinExclusivePosINFPass = test + @Test def doubleMinExclusiveNegINFFail = test + @Test def doubleMinExclusiveNegativeZeroFail = test + @Test def doubleMaxExclusiveNaNFail = test + @Test def doubleMaxExclusivePosINFFail = test + @Test def doubleMaxExclusiveNegINFPass = test + @Test def doubleMaxExclusiveNegativeZeroFail = test }