This is an automated email from the ASF dual-hosted git repository.
stevedlawrence pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/daffodil.git
The following commit(s) were added to refs/heads/main by this push:
new 45fc21ab7 Tolerate sub-millisecond differences in TDML dateTime
comparison
45fc21ab7 is described below
commit 45fc21ab73c1d22c83e22f66744de88aa12dfa15
Author: Guichard Desrosiers <[email protected]>
AuthorDate: Wed Jun 10 20:02:48 2026 -0400
Tolerate sub-millisecond differences in TDML dateTime comparison
ICU calendars are millisecond-precision, so fractional microseconds in
xs:dateTime values are not preserved on parse. Truncate fractional seconds
to milliseconds from the XMLGregorianCalendars before comparison, so
sub-millisecond differences are ignored rather than flagged as mismatches.
DAFFODIL-3086
---
.../org/apache/daffodil/lib/xml/XMLUtils.scala | 25 ++++++
.../section23/dfdl_functions/Functions.tdml | 89 +++++++++++++++++++++-
.../dfdl_expressions/TestDFDLExpressions.scala | 3 +
3 files changed, 116 insertions(+), 1 deletion(-)
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 702da577e..219a2c509 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
@@ -19,6 +19,7 @@ package org.apache.daffodil.lib.xml
import java.io.File
import java.io.IOException
+import java.math.RoundingMode
import java.net.URI
import java.net.URISyntaxException
import java.nio.charset.StandardCharsets
@@ -28,6 +29,7 @@ import java.nio.file.StandardOpenOption
import javax.xml.XMLConstants
import javax.xml.datatype.DatatypeConstants
import javax.xml.datatype.DatatypeFactory
+import javax.xml.datatype.XMLGregorianCalendar
import scala.annotation.tailrec
import scala.collection.mutable
import scala.collection.mutable.ArrayBuilder
@@ -1302,6 +1304,27 @@ Differences were (path, expected, actual):
}
}
+ /**
+ * Normalizes an XMLGregorianCalendar in place prior to comparison, so that
+ * values which are logically equal but differ in representation compare as
+ * equal. Mutates the calendar in place.
+ *
+ * Currently supported normalizations:
+ * - Fractional seconds: truncated to millisecond precision (3 digits),
+ * dropping sub-millisecond (microsecond) digits, since ICU calendars
+ * are millisecond-precision.
+ *
+ * Additional normalizations may be added here as needed.
+ *
+ * @param cal the calendar to normalize in place
+ */
+ private def normalizeXmlCalendar(cal: XMLGregorianCalendar): Unit = {
+ val frac = cal.getFractionalSecond
+ if (frac != null) {
+ cal.setFractionalSecond(frac.setScale(3, RoundingMode.DOWN))
+ }
+ }
+
/**
* Compares two XSD date/time lexical strings (`xs:date`, `xs:time`, or
* `xs:dateTime`) for value equality by parsing both into
`XMLGregorianCalendar`
@@ -1323,6 +1346,8 @@ Differences were (path, expected, actual):
private def dateTimeIsSame(dataA: String, dataB: String): Boolean = {
val a = datatypeFactory.newXMLGregorianCalendar(dataA)
val b = datatypeFactory.newXMLGregorianCalendar(dataB)
+ normalizeXmlCalendar(a)
+ normalizeXmlCalendar(b)
a.compare(b) == DatatypeConstants.EQUAL
}
diff --git
a/daffodil-test/src/test/resources/org/apache/daffodil/section23/dfdl_functions/Functions.tdml
b/daffodil-test/src/test/resources/org/apache/daffodil/section23/dfdl_functions/Functions.tdml
index 659135850..77db13fb1 100644
---
a/daffodil-test/src/test/resources/org/apache/daffodil/section23/dfdl_functions/Functions.tdml
+++
b/daffodil-test/src/test/resources/org/apache/daffodil/section23/dfdl_functions/Functions.tdml
@@ -14735,7 +14735,94 @@
</tdml:dfdlInfoset>
</tdml:infoset>
</tdml:parserTestCase>
-
+
+
+ <tdml:defineSchema name="MicrosecondsDateTime">
+ <xs:include
schemaLocation="/org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd"/>
+ <dfdl:format ref="ex:GeneralFormat" representation="text"
lengthKind="delimited" />
+
+ <xs:element name="timestampNano" type="xs:dateTime"
+ dfdl:calendarPatternKind="explicit"
+ dfdl:calendarPattern="yyyy-MM-dd'T'HH:mm:ss.SSSXXX" />
+ </tdml:defineSchema>
+
+ <!--
+ Test Name: datetimemicroseconds_01
+ Schema: MicrosecondsDateTime
+ Root: timestampNano
+ Purpose: Verifies the TDML runner ignores microsecond differences when
+ comparing xs:dateTime values. ICU calendars are
+ millisecond-precision, so microseconds are not preserved on
+ parse; the expected value here carries microsecond precision
+ that the parse drops. verifyParserTestData compares expected
vs.
+ actual via dateTimeIsSame, which uses XMLGregorianCalendar.
+ XMLGregorianCalendar does not tolerate microsecond differences
+ by default, so dateTimeIsSame normalizes each calendar to
+ millisecond precision (truncating sub-millisecond digits) after
+ construction and before comparison. This satisfies
DAFFODIL-3086.
+ Note: this value does not round-trip, since the dropped
+ microseconds cannot be reproduced on unparse.
+ -->
+ <tdml:parserTestCase name="datetimemicroseconds_01" root="timestampNano"
+ model="MicrosecondsDateTime"
+ description="TDML runner ignores fractional
microseconds in dateTime comparison"
+ roundTrip="none">
+ <tdml:document>2003-08-24T05:14:15.000-07:00</tdml:document>
+ <tdml:infoset>
+ <tdml:dfdlInfoset>
+ <ex:timestampNano>2003-08-24T05:14:15.000003-07:00</ex:timestampNano>
+ </tdml:dfdlInfoset>
+ </tdml:infoset>
+ </tdml:parserTestCase>
+
+ <!--
+ Test Name: datetimemicroseconds_02
+ Schema: MicrosecondsDateTime
+ Root: timestampNano
+ Purpose: Microsecond-precision xs:dateTime input parses and round-trips
+ under twoPass. ICU calendars are millisecond-precision, so the
+ sub-millisecond digits are not preserved: unparse emits the
+ millisecond value (differing from the original input, as twoPass
+ expects), and reparse yields the same infoset. Again, the TDML
+ Runner ignores the fractional-microsecond difference when
comparing
+ via dateTimeIsSame which uses the XMLGregorianCalendar.
+-->
+ <tdml:parserTestCase name="datetimemicroseconds_02" root="timestampNano"
+ model="MicrosecondsDateTime"
+ description="Microsecond dateTime round-trips under
twoPass;
+ runner ignores fractional microseconds in comparison"
+ roundTrip="twoPass">
+ <tdml:document>2003-08-24T05:14:15.000003-07:00</tdml:document>
+ <tdml:infoset>
+ <tdml:dfdlInfoset>
+ <ex:timestampNano>2003-08-24T05:14:15.000003-07:00</ex:timestampNano>
+ </tdml:dfdlInfoset>
+ </tdml:infoset>
+ </tdml:parserTestCase>
+
+ <!--
+ Test Name: datetimezeroMillis
+ Schema: MicrosecondsDateTime
+ Root: timestampNano
+ Purpose: A dateTime with all-zero milliseconds (.000) compares equal to
+ the same value with no fractional part. ICU renders zero
+ milliseconds as no fraction, so the TDML runner must treat
+ "...15.000-07:00" and "...15-07:00" as the same instant.
+ Verifies XMLGregorianCalendar treats .000 milliseconds and no
+ fractional part as the same instant; native behavior,
independent
+ of fractional-second truncation.
+-->
+ <tdml:parserTestCase name="datetimezeroMillis" root="timestampNano"
+ model="MicrosecondsDateTime"
+ description="dateTime with .000 milliseconds compares
equal to no fractional seconds">
+ <tdml:document>2003-08-24T05:14:15.000-07:00</tdml:document>
+ <tdml:infoset>
+ <tdml:dfdlInfoset>
+ <ex:timestampNano>2003-08-24T05:14:15-07:00</ex:timestampNano>
+ </tdml:dfdlInfoset>
+ </tdml:infoset>
+ </tdml:parserTestCase>
+
<!--
Test Name: timezonefromdate_01
Schema: Functions.dfdl.xsd
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 e0b118288..dead3d8de 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
@@ -781,6 +781,9 @@ class TestDFDLFunctions extends TdmlTests {
@Test def secondsfromdatetime_03 = test
@Test def timezonefromdatetime_01 = test
@Test def timezonefromdatetime_02 = test
+ @Test def datetimemicroseconds_01 = test
+ @Test def datetimemicroseconds_02 = test
+ @Test def datetimezeroMillis = test
@Test def xfromdatetime_01 = test
@Test def xfromdatetime_02 = test