6

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

我将问题缩小到确切的行,但我仍然无法弄清楚为什么它会这样,从文档中它指出儒略历用于公历开始之前的日期时间:1582 年 10 月 15 日。

问题线在演员 from XMLGregorianCalendarto GregorianCalendar,第 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);
        }
    }
}
4

2 回答 2

12

XMLGregorianCalendar.toGregorianCalendar()JavaDoc中关于他们如何创建GregorianCalendar实例的摘录:

通过调用 GregorianCalendar.setGregorianChange(new Date(Long.MIN_VALUE)) 获取纯公历。

这意味着,创建的日历将是预测性的,并且不会像默认情况下对旧日期那样切换到儒略历。那么问题就在这里:

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

有几种方法可以解决这个问题:

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

更新请注意,Java 日期和日历 API 的设计不是很好,有时可能(并且现在)很混乱。这也是 Java 8 包含完全重新设计的日期时间库JSR-310(顺便说一下基于 JodaTime)的原因。

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

  • 存储来自称为纪元(例如unix 纪元 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));

你可能会说:calendar.getTime()仍然给我错误的日期。好吧,那是因为java.util.Date.toString()(由 调用System.out.println)正在使用 default 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());

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

于 2014-04-14T18:49:06.303 回答
0

看起来好像您不小心从公历转换为儒略历。(外推或预测的)公历指的是 0001-01-01 的那一天实际上是儒略历中的 0001-01-03。如果将日期更改为 1001-01-01,您会发现最终结果是 1000-12-26。这与从公历到儒略的日期转换一致,因为偏移量随时间而变化。在 4 世纪之前它实际上是积极的,但从那以后一直是消极的(并且随着时间的推移变得更加消极)。

我得到的实例XMLGregorianCalendar实际上是 a com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImpl,该toGregorianCalendar()方法包括以下几行:

    result = new GregorianCalendar(tz, locale);
    result.clear();
    result.setGregorianChange(PURE_GREGORIAN_CHANGE);

这使得日历成为纯粹的预测 GregorianCalendar,因此 1583 年之前的日期仍然通过 Gregorian 算法从纪元时间(自 1970 年以来的毫秒数)转换。这意味着问题可能出在GregorianCalendar...

确实如此。试试这个示例程序,你会看到:

public static void main(String...args) {
    GregorianCalendar gcal = new GregorianCalendar();
    gcal.clear();
    gcal.setGregorianChange(new Date(Long.MIN_VALUE));
    gcal.set(Calendar.YEAR, 1);  // or any other year before 1582
    gcal.set(Calendar.MONTH, Calendar.JANUARY);
    gcal.set(Calendar.DATE, 1);

    Date d = gcal.getTime();
    System.out.println(d);
}

由于getGregorianChange()... 的值,进入的字段值被解释为公历,但出来(从纪元毫秒转换)该字段似乎被违反了。Date出来的没有getTime()格里高利转换值的概念。这可能是也可能不是错误,但这肯定是意外的行为。

于 2014-04-14T18:32:16.907 回答