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);
}
}
}
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.
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.
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());
There are few ways how to solve this:
LocalDate#fromCalendarFiels
if you are interested only in the date#getTime
methodUPDATE 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:
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:
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With