javagregorian-calendarxmlgregoriancalendar

ZonedDateTime to XMLGregorianCalendar and back does not match the original


Why doesn't this work?

    @Test
    public void test_calendar() throws DatatypeConfigurationException {
        ZonedDateTime zdt = ZonedDateTime.now();
        GregorianCalendar gc = GregorianCalendar.from(zdt);
        XMLGregorianCalendar xgc = DatatypeFactory.newInstance().newXMLGregorianCalendar(gc);
        assertThat(gc.toZonedDateTime(), comparesEqualTo(zdt));
        assertThat(xgc.toGregorianCalendar(), comparesEqualTo(gc));
        assertThat(xgc.toGregorianCalendar().toZonedDateTime(), comparesEqualTo(zdt)); // <== fails here
    }

The above code fails with:

Expected: a value equal to <2025-06-19T18:13:44.885-05:00[America/Chicago]>
     but: <2025-06-19T18:13:44.885-05:00[GMT-05:00]> was greater than <2025-06-19T18:13:44.885-05:00[America/Chicago]>

I see that the time zones don't match, but why don't the gc to zdt and xgc to gc fail?

Edit: removed superfluous code.


Solution

  • TL;DR

    XMLGregorianCalendar truncates fractional seconds and also returns the second type of ZoneId while ZonedDateTime returns the third type.
    The comparison could fail in either of both in that order.

    Details

    First, comparesEqualTo uses comparesTo of the ZonedDateTime object

    comparesEqualTo
    public static <T extends java.lang.Comparable> Matcher comparesEqualTo(T value)
    Creates a matcher of Comparable object that matches when the examined object is equal to the specified value, as reported by the compareTo method of the examined object.

    ZonedDateTime.compareTo() inherits from ChronoZonedDateTime.compareTo()

    The comparison is based first on the instant, then on the local date-time, then on the zone ID, then on the chronology.

    With code

    The comparison fails at instant level in this case but would also fail on zoneId level as can be seen on JetBrain source code or OpenJdk source code.

    import java.time.ZonedDateTime;
    import java.time.temporal.ChronoField;
    import java.util.GregorianCalendar;
    
    import javax.xml.datatype.DatatypeConfigurationException;
    import javax.xml.datatype.DatatypeFactory;
    import javax.xml.datatype.XMLGregorianCalendar;
    
    public class XmlDateTest {
    
        public static void main(String[] args) throws DatatypeConfigurationException {
            ZonedDateTime zdt = ZonedDateTime.now();
            GregorianCalendar gc = GregorianCalendar.from(zdt);
            XMLGregorianCalendar xgc = DatatypeFactory.newInstance().newXMLGregorianCalendar(gc);
            System.out.println("zdt\t" + zdt);
    
            System.out.println("zdt_fraction\t" + zdt.getLong(ChronoField.NANO_OF_SECOND) / 1E9);
    
            System.out.println("xgc_fraction\t" + xgc.getFractionalSecond());
                    // zero if equal
            System.out.println(
                    "zdt equals xgc? " + zdt.toInstant().compareTo(xgc.toGregorianCalendar().toZonedDateTime().toInstant()));
    
                    // zone id is also reported as a different string even if both are logically equivalent
                    System.out.println("zdt zone id\t" + zdt.getZone().getId());
                    System.out.println("xgc zone id\t" + xgc.toGregorianCalendar().toZonedDateTime().getZone().getId());
    
            System.out.println(System.getProperty("os.name"));
            System.out.println(System.getProperty("java.runtime.name"));
            System.out.println(System.getProperty("java.version"));
    
        }
    }
    

    Result

    zdt 2025-06-19T21:37:32.676844595-03:00[America/Argentina/Mendoza]
    zdt_fraction    0.676844595
    xgc_fraction    0.676
    zdt equals xgc? 844595
    
    
    zdt zone id America/Argentina/Mendoza
    xgc zone id GMT-03:00
    

    As an special use case, there's no difference at start of day since there are no fractional seconds involved but compareTo might be triggering on ZoneId.

    zdt = ZonedDateTime.of(LocalDate.now().atStartOfDay(), ZoneId.systemDefault());

    zdt 2025-06-20T00:00-03:00[America/Argentina/Mendoza]
    zdt_nanos   0.0
    xgc_nanos   0.000
    zdt equals xgc? 0
    
    
    zdt zone id America/Argentina/Mendoza
    xgc zone id GMT-03:00
    

    Test env

    Linux
    OpenJDK Runtime Environment
    22.0.1
    
    Linux
    OpenJDK Runtime Environment
    17.0.15
    

    NOTE: time precision might depend on OS and JVM version