Date change when converting from XMLGregorianCalendar to Calendar

19,358

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).

Share:
19,358
JWiley
Author by

JWiley

Updated on June 28, 2022

Comments

  • JWiley
    JWiley almost 2 years

    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);
            }
        }
    }
    
  • JWiley
    JWiley about 10 years
    Thanks Pavel. I unfortunately can't use Joda in this situation, can you show bullets 2 and 3 please?
  • Pavel Horal
    Pavel Horal about 10 years
    I've updated the answer with some background information on Date and Calendar interactions. Also I have deleted the second bullet as it was not correct advice and would lead you into more problems.
  • JWiley
    JWiley about 10 years
    That last part just made it more confusing. How would I apply your solution to what I have? In other words where would I incorporate ((GregorianCalendar) calendar).setGregorianChange(new Date(Long.MIN_VALUE)); in my setXMLGregorianCalendar method and use it to get the date from the GregorianCalendar variable?
  • Pavel Horal
    Pavel Horal about 10 years
    You should call this method every time you create or obtain new Calendar instance.
  • Pavel Horal
    Pavel Horal about 10 years
    Also the point I was trying to make was that the date was never incorrect... it was just displayed in Julian system. Conversion to proleptic Gregorian can be made just before you display it to user if needed. But there is nothing wrong if you force every calendar in your application to be proleptic. However Date#toString will probably always switch to Julian and display "incorrect" value.
  • JWiley
    JWiley about 10 years
    I have to send a type of Date so I can't convert it back to Gregorian, the methods shown in my code are the ones I have to use. So, can you either 1) Show how to force the calendars to be proleptic or 2) Show how to properly "call this method every time you create or obtain new Calendar instance" as what you provided does not resolve my issue when used in the example I provided.
  • Pavel Horal
    Pavel Horal about 10 years
  • Pavel Horal
    Pavel Horal about 10 years
    @JWiley I've added the wanted example of "incorrect" calendar conversion. Now that I am sure you understand the consequences I am no longer ashamed for that :).