简体   繁体   English

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

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

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

The problem line is at the cast from XMLGregorianCalendar to GregorianCalendar , line 78: calendarDate = argCal.toGregorianCalendar(); 问题行是从XMLGregorianCalendarGregorianCalendar ,第78行: calendarDate = argCal.toGregorianCalendar(); When the time is taken from calendarDate on line 86: cal.setTime(calendarDate.getTime()); 当从第86行的calendarDate获取时间时: 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. 时间比1月3日而不是1月1日提前2天回来,正如您将从下面的程序输出中看到的那样。

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);
        }
    }
}

Excerpt from XMLGregorianCalendar.toGregorianCalendar() JavaDoc on how they create the GregorianCalendar instance: 摘自XMLGregorianCalendar.toGregorianCalendar() JavaDoc ,了解它们如何创建GregorianCalendar实例:

Obtain a pure Gregorian Calendar by invoking GregorianCalendar.setGregorianChange( new Date(Long.MIN_VALUE)). 通过调用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. 这意味着,创建的日历将是无效的,并且不会像旧日期那样切换到Julian日历。 Then the problem is here: 那问题就在这里:

  • argCal.toGregorianCalendar() - converting from XMLGregorianCalendar to GregorianCalendar using field representation (Julian system is not used - see above) argCal.toGregorianCalendar() - 使用字段表示XMLGregorianCalendar转换为GregorianCalendar (不使用Julian系统 - 见上文)
  • 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 新日历使用Julian系统来表示日期,因为它早于1582年

There are few ways how to solve this: 解决这个问题的方法很简单:

  • use JodaTime and LocalDate#fromCalendarFiels if you are interested only in the date 如果您只对日期感兴趣,请使用JodaTime和LocalDate#fromCalendarFiels
  • convert calendars using field access and not the #getTime method 使用字段访问而不是#getTime方法转换日历
  • force the gregorian calendar to use proleptic system (in the same way as XMLGregorianCalendar is doing it) 强制格里高利历使用proleptic系统(与XMLGregorianCalendar一样)

UPDATE Please note that Java Date and Calendar APIs are not so well designed and can be (and are) sometimes pretty confusing. 更新请注意,Java日期和日历API的设计并不是很好,有时可能会(并且有时)令人困惑。 This is also why Java 8 contains completely reworked date-time library JSR-310 (based on JodaTime by the way). 这也是为什么Java 8包含完全重写的日期时间库JSR-310 (顺便说一下基于JodaTime)的原因。

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 (eg in milliseconds) from a well defined instant called epoch (eg unix epoch 1970-01-01 ) 从称为epoch的明确定义的瞬间存储偏移量(例如以毫秒为单位)(例如unix epoch 1970-01-01
  • storing date by its calendar fields (eg 1st of January 1970) 按日历字段存储日期(例如1970年1月1日)

The first approach is what is being used under the hood in java.util.Date . 第一种方法是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. 有些国家可以决定使用阴历月,其他人可能会说0年仅仅是10年前。 And gregorian calendar is just one way of converting actual instant to actual date fields. 格里高利历只是将实际即时字段转换为实际日期字段的一种方式。

A bit on XMLGregorianCalendar vs GregorianCalendar : 关于XMLGregorianCalendarGregorianCalendar的一点点:

  • XML specification explicitly says that the human-readable date is a gregorian calendar date XML规范明确指出人类可读日期是格里历日历日期
  • 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 Java的GregorianCalendar包含这个“魔法” ,如果瞬间比定义的切换日期更早,它将切换到引擎盖下的Julian系统
  • that is why XMLGregorianCalendar modifies GregorianCalendar during its initialization to disable this magic switch (see the excerpt from JavaDoc above) 这就是为什么XMLGregorianCalendar在初始化期间修改GregorianCalendar以禁用这个魔术开关(参见上面的JavaDoc摘录)

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. 如果Julian开关不会被禁用,那么GregorianCalendar会假设日历字段来自Julian系统,它会将它们移动3天。 You thought that the date has been shifted by 3 days and something must've went wrong, right? 你认为这个日期已经改变了3天而且肯定会出现问题,对吧? 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]. 我觉得这很令人困惑:) [JSR-310在后台笑]。

So if you want to work with pure gregorian calendar (ie to use so called proleptic gregorian for old dates), you need to initialize calendar like this: 因此,如果你想使用纯格里高利历(即在旧日期使用所谓的proleptic gregorian ),你需要像这样初始化日历:

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

You might say: calendar.getTime() is still giving me incorrect date. 你可能会说: calendar.getTime()仍然给我错误的日期。 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. 好吧,那是因为java.util.Date.toString() (由System.out.println调用)正在使用默认Calendar ,它将切换到旧日期的Julian系统。 Confused? 困惑? Maybe angry (I know I am :))? 也许生气(我知道我是:))?


UPDATE 2 更新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). 免责声明: 此代码错误 (结果瞬间与XML文件中的结果不同),但OP了解问题及其后果(请参阅本答案下的讨论)。

It looks as though you are accidentally converting from the Gregorian Calendar to the Julian Calendar. 看起来好像你不小心从格里高利历转换到朱利安日历。 The day that the (extrapolated or proleptic ) Gregorian Calendar refers to as 0001-01-01 is, indeed, 0001-01-03 in the Julian Calendar. (外推或延长的 )阳历称为0001-01-01的那一天确实是朱利安日历中的0001-01-03。 If you change your date to 1001-01-01, you will find that your end result is 1000-12-26. 如果您将日期更改为1001-01-01,您会发现最终结果是1000-12-26。 This is consistent with date conversions from Gregorian to Julian, as the offset changes over time. 这与从格里高利到朱利安的日期转换一致,因为偏移随时间变化。 It was actually positive before the 4th century, but has been negative (and getting more negative over time) since then. 它在4世纪之前实际上是积极的,但从那以后一直是消极的(随着时间的推移变得越来越消极)。

The instance of XMLGregorianCalendar I get is actually a com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImpl and the toGregorianCalendar() method includes these lines: 我得到的XMLGregorianCalendar的实例实际上是com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImpl ,而toGregorianCalendar()方法包括以下这些行:

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

This makes the calendar an purely proleptic GregorianCalendar, so that dates before 1583 are still converted from the epoch time (milliseconds since 1970) by the Gregorian algorithm. 这使得日历成为纯粹的历史GregorianCalendar,因此1583年之前的日期仍然通过格里高利算法从纪元时间(自1970年以来的毫秒)转换。 Which means the problem is probably in GregorianCalendar ... 这意味着问题可能出在GregorianCalendar ......

Which is indeed the case. 确实如此。 Try this sample program and you'll see: 试试这个示例程序,你会看到:

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);
}

The field values going in are interpreted as Gregorian due to the value of getGregorianChange() ... but coming out (converting from epoch milliseconds) that field seems to be disobeyed. 由于getGregorianChange()的值, getGregorianChange()的字段值被解释为Gregorian,但是出现(从epoch毫秒转换)该字段似乎是不服从的。 The Date that comes out of getTime() has no concept of what the Gregorian changeover value was. getTime()出来的Date没有Gregorian转换值的概念。 This may or may not be a bug, but it's certainly unexpected behaviour. 这可能是也可能不是错误,但它肯定是意外行为。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM