从XMLGregorianCalendar转换为Calendar时的日期更改

JWi*_*ley 6 java datetime casting

当测试在系统之间映射日期时间类型的Web服务时,我注意到在公历开始时间之前发送任何日期会导致在转换为最终类型时失去准确性,最终结果总是在范围内略微提前几天.

我把问题缩小到了确切的界限,但是我仍然无法弄清楚为什么它会像这样投射,从文件中说明朱利安日历用于格里高利历开始之前的日期时间:1582年10月15日.

这个问题行是在与流延XMLGregorianCalendarGregorianCalendar,行78:calendarDate = argCal.toGregorianCalendar(); 当时间摘自calendarDate在线86:cal.setTime(calendarDate.getTime());时间回来在它前面的应该是什么2天后,1月3日而不是1月1日,你会看到从下面的程序输出.

这是我制作的示例程序,用于显示端到端的铸造过程:

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Pav*_*ral 11

摘自XMLGregorianCalendar.toGregorianCalendar()JavaDoc,了解他们如何创建GregorianCalendar实例:

通过调用GregorianCalendar.setGregorianChange(new Date(Long.MIN_VALUE))获取纯格里高利历.

这意味着,创建的日历将是无效的,并且不会像旧日期那样切换到Julian日历.那问题就在这里:

  • argCal.toGregorianCalendar()- 使用字段表示XMLGregorianCalendar转换为GregorianCalendar (不使用Julian系统 - 见上文)
  • cal.setTime(calendarDate.getTime());
    • 这实际上是将字段表示转换为时间戳表示并使用此时间戳初始化新日历
    • 新日历使用Julian系统来表示日期,因为它早于1582年

解决这个问题的方法很简单:

  • 使用JodaTime,LocalDate#fromCalendarFiels如果您只对日期感兴趣
  • 使用字段访问而不是#getTime方法转换日历
  • 强制格里高利历使用proleptic系统(与XMLGregorianCalendar一样)

更新请注意,Java日期和日历API的设计并不是很好,有时可能会(并且有时)令人困惑.这也是为什么Java 8包含完全重写的日期时间库JSR-310(顺便说一下基于JodaTime)的原因.

现在,您必须意识到,您可以通过两种截然不同的方法存储和使用特定的即时(日历独立关键字):

  • 从称为epoch 的明确定义的瞬间存储偏移量(例如以毫秒为单位)(例如unix epoch 1970-01-01)
  • 按日历字段存储日期(例如1970年1月1日)

第一种方法是在引擎盖下使用的方法java.util.Date.然而,这种表示通常是非人类友好的.人类使用日历日期,而不是时间戳.将时间戳转换为日期字段是日历的步骤.也就是有趣的部分开始的地方......如果你想用它的字段表示日期,你需要意识到总有多种方法可以做到这一点.有些国家可以决定使用阴历月,其他人可能会说0年仅仅是10年前.格里高利历只是将实际即时字段转换为实际日期字段的一种方式.

关于XMLGregorianCalendarGregorianCalendar的一点点:

  • XML规范明确指出人类可读日期是格里历日历日期
  • Java的GregorianCalendar包含这个"魔法",如果瞬间比定义的切换日期更早,它将切换到引擎盖下的Julian系统
  • 这就是为什么XMLGregorianCalendar在初始化期间修改GregorianCalendar以禁用这个魔术开关(参见上面的JavaDoc摘录)

现在有趣的部分:

如果Julian开关不会被禁用,GregorianCalendar会假设日历字段来自Julian系统,它将把它们移动3天.你认为这个日期已经改变了3天而且肯定会出现问题,对吧?不,日期实际上一直都是正确的,它包含正确的时间戳!只有日历给你呈现了朱利安领域而不是格里高利领域.我觉得这很令人困惑:) [JSR-310在后台笑].

因此,如果你想使用纯格里高利历(即在旧日期使用所谓的proleptic gregorian),你需要像这样初始化日历:

Calendar calendar = Calendar.getInstance();
((GregorianCalendar) calendar).setGregorianChange(new Date(Long.MIN_VALUE));
Run Code Online (Sandbox Code Playgroud)

你可能会说:calendar.getTime()仍然给我错误的日期.好吧,那是因为java.util.Date.toString()(被称为System.out.println)使用默认值Calendar,它将切换到旧日期的Julian系统.困惑?也许生气(我知道我是:))?


更新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());
Run Code Online (Sandbox Code Playgroud)

免责声明:此代码错误(结果瞬间与XML文件中的结果不同),但OP了解问题及其后果(请参阅本答案下的讨论).