Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Date change when converting from XMLGregorianCalendar to Calendar

When testing out a web service that maps datetime types between systems, I noticed that sending any date before the Gregorian calendar start time resulted in a loss of accuracy when casting to the final type, with the end result always slightly ahead in time in the range of a few days.

I narrowed down the problem to the exact line, but I still can't figure out why it's being cast like so, from the documentation it states that the Julian calendar is used for datetimes before the Gregorian calendar start: October 15, 1582.

The problem line is at the cast from XMLGregorianCalendar to GregorianCalendar, line 78: calendarDate = argCal.toGregorianCalendar(); When the time is taken from calendarDate on line 86: cal.setTime(calendarDate.getTime()); The time comes back 2 days ahead of what it should be, Jan. 03 instead of Jan. 01, as you'll see from the output in the program below.

Here's a sample program I made to show the casting process end to end:

import java.sql.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;



public class TestDateConversions {

    public static void main(String[] args)
    {
        TestDateConversions testDates = new TestDateConversions();
        try
        {
            XMLGregorianCalendar testDate1 = DatatypeFactory.newInstance().newXMLGregorianCalendar();
            testDate1.setYear(0001);
            testDate1.setMonth(01);
            testDate1.setDay(01);
            System.out.println("Start date: "+testDate1.toString() +"\n**********************");

            testDates.setXMLGregorianCalendar(testDate1);
            System.out.println("\nNull given \n"+ "**********");
            testDates.setXMLGregorianCalendar(null);
        }
        catch(Exception e)
        {
            System.out.println(e);
        }
    }


    public void setXMLGregorianCalendar(XMLGregorianCalendar argCal)
    {
        GregorianCalendar calendarDate;
        if (argCal != null)
        {
            calendarDate = argCal.toGregorianCalendar();
            System.out.println("XMLGregorianCalendar time: " + argCal.getHour() + ":"+argCal.getMinute()+":"+argCal.getSecond());
            System.out.println("XMLGregorianCalendar time(ms): "+argCal.getMillisecond());
            System.out.println("XMLGregorianCalendar -> GregorianCalendar: "+calendarDate.get(GregorianCalendar.YEAR) + "-"+(calendarDate.get(GregorianCalendar.MONTH)+1) + "-"+calendarDate.get(GregorianCalendar.DAY_OF_MONTH));
            System.out.println("!!!!PROBLEM AREA!!!!");
            Calendar cal = Calendar.getInstance();
            System.out.println("-- New Calendar instance: "+cal.get(Calendar.YEAR) + "-"+(cal.get(Calendar.MONTH)+1)+"-"+cal.get(Calendar.DAY_OF_MONTH));
            System.out.println("-- Calling Calendar.setTime(GregorianCalendar.getTime())");
            cal.setTime(calendarDate.getTime());
            System.out.println("-- calendarDate.getTime() = " + calendarDate.getTime() + " <-- time is incorrect");
            System.out.println("-- Calendar with time set from GregorianCalendar: "+cal.get(Calendar.YEAR) + "-"+(cal.get(Calendar.MONTH)+1)+"-"+cal.get(Calendar.DAY_OF_MONTH) + " <-- day is increased here");
            setCalendar(cal);
        }
        else 
        {
            setCalendar(null);
        }
    }

    public void setCalendar(Calendar argCal)
    {
        if (argCal != null)
        {
            Date date = new Date(argCal.getTimeInMillis());
            System.out.println("Calendar to Date: "+date);
            setDate(date);
        }
        else
        {
            setDate(null);
        }

    }

    public void setDate(Date argDate)
    {
        try
        {
            if (argDate == null)
            {
                Calendar cal  = new GregorianCalendar(1,0,1);
                Date nullDate = new Date(cal.getTimeInMillis());
                System.out.println("Null Calendar created: "+cal.get(Calendar.YEAR) + "-"+(cal.get(Calendar.MONTH)+1)+"-"+cal.get(Calendar.DAY_OF_MONTH));
                System.out.println("Null Date created: "+nullDate);
            }
            else 
            {
                System.out.println("Final date type: "+argDate);
            }
        }
        catch (Exception  ex)
        {
            System.out.println(ex);
        }
    }
}
like image 362
JWiley Avatar asked May 01 '13 15:05

JWiley


People also ask

How to convert XMLGregorianCalendar to Calendar in Java?

The simplest way to format XMLGregorianCalendar is to first convert it to the Date object, and format the Date to String. XMLGregorianCalendar xCal = ..; //Create instance Date date = xCal. toGregorianCalendar(). getTime(); DateFormat df = new SimpleDateFormat("MM/dd/yyyy hh:mm a z"); String formattedString = df.

What is XML Gregorian calendar?

XML Gregorian Calendar: The rules for specifying dates in XML format are defined in the XML Schema standard. The Java XMLGregorianCalendar class, introduced in Java 1.5, is a representation of the W3C XML Schema 1.0 date/time datatypes and is required to use the XML format.


1 Answers

Excerpt from XMLGregorianCalendar.toGregorianCalendar() JavaDoc on how they create the GregorianCalendar instance:

Obtain a pure Gregorian Calendar by invoking GregorianCalendar.setGregorianChange( new Date(Long.MIN_VALUE)).

This means, that the created calendar will be proleptic and won't switch to Julian calendar as it does by default for old dates. Then the problem is here:

  • argCal.toGregorianCalendar() - converting from XMLGregorianCalendar to GregorianCalendar using field representation (Julian system is not used - see above)
  • cal.setTime(calendarDate.getTime());
    • this is actually converting field representation to a timestamp representation and initializing a new calendar with this timestamp
    • the new calendar is using Julian system to represent the date as it is older than 1582

There are few ways how to solve this:

  • use JodaTime and LocalDate#fromCalendarFiels if you are interested only in the date
  • convert calendars using field access and not the #getTime method
  • force the gregorian calendar to use proleptic system (in the same way as XMLGregorianCalendar is doing it)

UPDATE Please note that Java Date and Calendar APIs are not so well designed and can be (and are) sometimes pretty confusing. This is also why Java 8 contains completely reworked date-time library JSR-310 (based on JodaTime by the way).

Now, you have to realize, that you can store and work with a specific instant (calendar independent keyword) via two very different approaches:

  • storing an offset (e.g. in milliseconds) from a well defined instant called epoch (e.g. unix epoch 1970-01-01)
  • storing date by its calendar fields (e.g. 1st of January 1970)

The first approach is what is being used under the hood in java.util.Date. However this representation is usually non-human friendly. Humans work with calendar dates, not timestamps. Converting timestamps to date fields is where Calendar steps in. Also that is where the funny part starts... if you want to represent date by its fields, you need to realize that there are always multiple ways how to do that. Some nation can decide to use lunar months, others may say that the year 0 was just 10 years ago. And gregorian calendar is just one way of converting actual instant to actual date fields.

A bit on XMLGregorianCalendar vs GregorianCalendar:

  • XML specification explicitly says that the human-readable date is a gregorian calendar date
  • Java's GregorianCalendar contains this "magic", which switches to Julian system under the hood, if the instant is older than a defined switch-over date
  • that is why XMLGregorianCalendar modifies GregorianCalendar during its initialization to disable this magic switch (see the excerpt from JavaDoc above)

Now the interesting part:

If the Julian switch won't be disabled, GregorianCalendar would assume that the calendar fields are from Julian system and it will shift them by 3 days. You thought that the date has been shifted by 3 days and something must've went wrong, right? No, the date was actually all the time correct and it contained correct timestamp under the hood! Only the calendar had presented you Julian fields instead of Gregorian fields. And this is pretty confusing I would say :) [JSR-310 laughing in the background].

So if you want to work with pure gregorian calendar (i.e. to use so called proleptic gregorian for old dates), you need to initialize calendar like this:

Calendar calendar = Calendar.getInstance();
((GregorianCalendar) calendar).setGregorianChange(new Date(Long.MIN_VALUE));

You might say: calendar.getTime() is still giving me incorrect date. Well, that is because java.util.Date.toString() (called by System.out.println) is using the default Calendar, which will switch to Julian system for older dates. Confused? Maybe angry (I know I am :))?


UPDATE 2

// Get XML gregorian calendar
XMLGregorianCalendar xmlCalendar = DatatypeFactory.newInstance().newXMLGregorianCalendar();
xmlCalendar.setYear(1); // Watch for octal number representations (you had there 0001)
xmlCalendar.setMonth(1);
xmlCalendar.setDay(1);

// Convert to Calendar as it is easier to work with it
Calendar calendar = xmlCalendar.toGregorianCalendar(); // Proleptic for old dates

// Convert to default calendar (will misinterpret proleptic for Julian, but it is a workaround)
Calendar result = Calendar.getInstance();
result.setTimeZone(calendar.getTimeZone());
result.set(Calendar.YEAR, calendar.get(Calendar.YEAR));
result.set(Calendar.MONTH, calendar.get(Calendar.MONTH));
result.set(Calendar.DAY_OF_MONTH, calendar.get(Calendar.DAY_OF_MONTH));
result.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY));
result.set(Calendar.MINUTE, calendar.get(Calendar.MINUTE));
result.set(Calendar.SECOND, calendar.get(Calendar.SECOND));
result.set(Calendar.MILLISECOND, calendar.get(Calendar.MILLISECOND));

System.out.println(result.getTime());

Disclamer: this code is wrong (the result instant is not the same as the one in the XML file), but OP understands the problem and its consequences (see the discussion under this answer).

like image 55
Pavel Horal Avatar answered Sep 20 '22 23:09

Pavel Horal