From 767168bba9c88f54ee87f26e342dbc58c68cb84f Mon Sep 17 00:00:00 2001 From: Guichard Desrosiers Date: Mon, 1 Jun 2026 23:34:26 -0400 Subject: [PATCH] Replace ICU-based date/time comparison in TDML Runner with XMLGregorianCalendar The TDML date/time comparison previously used the DFDLDate/Time/DateTimeConversion classes, which create ICU objects. This broke cross-testing against the IBM DFDL cross tester, which depends on an older ICU version (e.g. it lacks Calendar.clone()). Compare xs:date/time/dateTime values by parsing into XMLGregorianCalendar and using its XSD order relation (compare()), keeping ICU off the comparison path entirely. XMLGregorianCalendar implements XSD 1.0, which does not permit year zero, so it rejects values like "0000-01-01". Two tests whose data uses year zero (yearfromdate_03 and yearfromdatetime_03) are temporarily ignored as a result. These expose a pre-existing Daffodil bug: Daffodil produces year-zero values that are invalid under XSD 1.0, which needs to be addressed separately. The tests should be re-enabled (or converted to negative tests) once that is resolved. DAFFODIL-3077 --- .../apache/daffodil/lib/xml/XMLUtils.scala | 48 ++++++++++++------- .../TestDFDLExpressions.scala | 4 +- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/lib/xml/XMLUtils.scala b/daffodil-core/src/main/scala/org/apache/daffodil/lib/xml/XMLUtils.scala index d2a8179126..702da577ef 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/lib/xml/XMLUtils.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/lib/xml/XMLUtils.scala @@ -26,6 +26,8 @@ import java.nio.file.Files import java.nio.file.Paths import java.nio.file.StandardOpenOption import javax.xml.XMLConstants +import javax.xml.datatype.DatatypeConstants +import javax.xml.datatype.DatatypeFactory import scala.annotation.tailrec import scala.collection.mutable import scala.collection.mutable.ArrayBuilder @@ -33,9 +35,6 @@ import scala.math.abs import scala.util.matching.Regex import scala.xml.* -import org.apache.daffodil.lib.calendar.DFDLDateConversion -import org.apache.daffodil.lib.calendar.DFDLDateTimeConversion -import org.apache.daffodil.lib.calendar.DFDLTimeConversion import org.apache.daffodil.lib.exceptions.* import org.apache.daffodil.lib.iapi.DaffodilSchemaSource import org.apache.daffodil.lib.iapi.URISchemaSource @@ -54,6 +53,9 @@ import org.xml.sax.XMLReader object XMLUtils { + // DatatypeFactory creation is relatively expensive, so create it once and reuse. + private lazy val datatypeFactory = DatatypeFactory.newInstance() + lazy val schemaForDFDLSchemas = Misc.getRequiredResource("org/apache/daffodil/xsd/XMLSchema_for_DFDL.xsd") @@ -1300,6 +1302,30 @@ Differences were (path, expected, actual): } } + /** + * Compares two XSD date/time lexical strings (`xs:date`, `xs:time`, or + * `xs:dateTime`) for value equality by parsing both into `XMLGregorianCalendar` + * and comparing via the XSD `·order·` relation. + * + * Note that we intentionally do not use Daffodil's DFDL*Conversion.fromXMLString + * classes which keeps ICU off the comparison path entirely and allows the + * IBM DFDL cross tester (pinned to an older ICU version) to share this code without + * hitting newer-ICU-only methods (DAFFODIL-3077). + * + * @param dataA the first value's lexical string + * @param dataB the second value's lexical string + * @return true if the two values are equal under the XSD order relation + * @throws IllegalArgumentException if either string is not a valid lexical + * representation of an XSD 1.0 date/time. + * + * @throws NullPointerException if either string is null + */ + private def dateTimeIsSame(dataA: String, dataB: String): Boolean = { + val a = datatypeFactory.newXMLGregorianCalendar(dataA) + val b = datatypeFactory.newXMLGregorianCalendar(dataB) + a.compare(b) == DatatypeConstants.EQUAL + } + /** * Compares two strings of xml text, optionally using type information to tolerate insignificant differences, and * optionally using a tolerance amount for floating point comparison. @@ -1326,20 +1352,8 @@ Differences were (path, expected, actual): maybeType match { case Some("xs:hexBinary") => dataA.equalsIgnoreCase(dataB) - case Some("xs:date") => { - val a = DFDLDateConversion.fromXMLString(dataA) - val b = DFDLDateConversion.fromXMLString(dataB) - a == b - } - case Some("xs:time") => { - val a = DFDLTimeConversion.fromXMLString(dataA) - val b = DFDLTimeConversion.fromXMLString(dataB) - a == b - } - case Some("xs:dateTime") => { - val a = DFDLDateTimeConversion.fromXMLString(dataA) - val b = DFDLDateTimeConversion.fromXMLString(dataB) - a == b + case Some("xs:date") | Some("xs:time") | Some("xs:dateTime") => { + dateTimeIsSame(dataA, dataB) } case Some("xs:double") => { val a = strToDouble(dataA) diff --git a/daffodil-test/src/test/scala/org/apache/daffodil/section23/dfdl_expressions/TestDFDLExpressions.scala b/daffodil-test/src/test/scala/org/apache/daffodil/section23/dfdl_expressions/TestDFDLExpressions.scala index 1f24cd24e1..77ccb80b95 100644 --- a/daffodil-test/src/test/scala/org/apache/daffodil/section23/dfdl_expressions/TestDFDLExpressions.scala +++ b/daffodil-test/src/test/scala/org/apache/daffodil/section23/dfdl_expressions/TestDFDLExpressions.scala @@ -766,7 +766,7 @@ class TestDFDLFunctions extends TdmlTests { @Test def yearfromdatetime_01 = test @Test def yearfromdatetime_02 = test - @Test def yearfromdatetime_03 = test + @Ignore @Test def yearfromdatetime_03 = test @Test def monthfromdatetime_01 = test @Test def monthfromdatetime_02 = test @Test def dayfromdatetime_01 = test @@ -788,7 +788,7 @@ class TestDFDLFunctions extends TdmlTests { @Test def yearfromdate_01 = test @Test def yearfromdate_02 = test - @Test def yearfromdate_03 = test + @Ignore @Test def yearfromdate_03 = test @Test def monthfromdate_01 = test @Test def monthfromdate_02 = test @Test def dayfromdate_01 = test