6

システム間で datetime 型をマップする Web サービスをテストしたとき、グレゴリオ暦の開始時刻より前の日付を送信すると、最終的な型にキャストするときに精度が失われ、最終的な結果が常に範囲内でわずかに進んでいることに気付きました。数日の。

問題を正確な行に絞り込みましたが、なぜそのようにキャストされているのかまだわかりません。ドキュメントから、ユリウス暦はグレゴリオ暦の開始前の日付時刻に使用されていると記載されています: 1582 年 10 月 15 日.

XMLGregorianCalendar問題の行は from toGregorianCalendarの 78 行目のキャストにあります: 時刻が86 行calendarDate = argCal.toGregorianCalendar(); 目から取得される場合:時刻は 1 月 01 日ではなく 1 月 03 日より 2 日早く返されます。以下のプログラムの出力から。calendarDatecal.setTime(calendarDate.getTime());

以下は、キャスト プロセスを端から端まで示すために作成したサンプル プログラムです。

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 年より古いため、日付を表すためにユリウス システムを使用しています。

これを解決する方法はいくつかあります。

  • JodaTime を使用LocalDate#fromCalendarFielsし、日付のみに関心がある場合
  • #getTimeメソッドではなくフィールド アクセスを使用してカレンダーを変換する
  • グレゴリオ暦に先発システムを強制的に使用させる ( XMLGregorianCalendarが行っているのと同じ方法で)

更新Java Date および Calendar API はあまり適切に設計されておらず、かなり混乱する可能性があることに注意してください。これは、Java 8 が完全に作り直された日時ライブラリJSR-310 (ちなみに JodaTime に基づく) を含む理由でもあります。

ここで、 2 つの非常に異なる方法で特定のインスタント(カレンダーに依存しないキーワード) を保存して操作できることを認識する必要があります。

  • エポックと呼ばれる明確に定義された瞬間からのオフセット (ミリ秒単位など) を格納する(例: unix epoch 1970-01-01 )
  • カレンダーフィールドによる日付の保存 (例: 1970 年 1 月 1 日)

最初のアプローチは、 の内部で使用されているものですjava.util.Date。ただし、この表現は通常、人間に優しくありません。人間は、タイムスタンプではなく、カレンダーの日付を操作します。タイムスタンプを日付フィールドに変換することは、Calendar が介入するところです。また、ここから面白い部分が始まります...日付をそのフィールドで表現したい場合は、それを行う方法が常に複数あることを認識する必要があります。太陰月を使用する国もあれば、0 年がちょうど 10 年前であると言う国もあります。グレゴリオ暦は、実際のインスタントを実際の日付フィールドに変換する 1 つの方法にすぎません。

XMLGregorianCalendarとGregorianCalendarの比較:

  • XML 仕様では、人間が判読できる日付はグレゴリオ暦の日付であることが明示されています
  • Java のGregorianCalendarにはこの「魔法」が含まれており、インスタントが定義された切り替え日よりも古い場合、ボンネットの下でユリウス システムに切り替えます。
  • そのため、XMLGregorianCalendarは初期化中にGregorianCalendarを変更して、この魔法のスイッチを無効にします (上記の JavaDoc からの抜粋を参照)。

今興味深い部分:

ジュリアン スイッチが無効にされない場合、GregorianCalendarはカレンダー フィールドがユリウス システムからのものであると想定し、それらを 3 日シフトします。日付が 3 日ずれて、何か問題が発生したに違いないと思いましたよね? いいえ、日付は実際には常に正確であり、フードの下に正しいタイムスタンプが含まれていました! グレゴリオ暦の代わりにユリウス暦を提示したのはカレンダーだけでした。そして、これはかなり紛らわしいと思います:) [バックグラウンドでJSR-310が笑っている]。

したがって、純粋なグレゴリオ暦で作業する場合 (つまり、古い日付にいわゆる先発グレゴリオ暦を使用する場合)、次のようにカレンダーを初期化する必要があります。

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

あなたは言うかもしれません:calendar.getTime()はまだ私に間違った日付を与えています. これは、java.util.Date.toString()(によって呼び出されたSystem.out.println) がデフォルトCalendarの を使用しているためです。これにより、古い日付のユリウス システムに切り替わります。混乱している?多分怒っています(私は私が知っています:))?


更新 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 世紀以前は実際にはプラスでしたが、それ以降はマイナスになっています (そして、時間の経過とともにマイナスになっています)。

I getのインスタンスXMLGregorianCalendarは実際には acom.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImplであり、toGregorianCalendar()メソッドには次の行が含まれます。

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

これにより、カレンダーは純粋に先発的な GregorianCalendar になり、1583 年より前の日付は引き続き、グレゴリオ アルゴリズムによってエポック時間 (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 に答える