startDateにPeriodを追加してもendDateは生成されません


8

次のように2つLocalDateのを宣言しています。

val startDate = LocalDate.of(2019, 10, 31)  // 2019-10-31
val endDate = LocalDate.of(2019, 9, 30)     // 2019-09-30

次に、Period.between関数を使用してそれらの間の期間を計算します。

val period = Period.between(startDate, endDate) // P-1M-1D

ここで、期間はその与えられた期待されている月と日の負の量、持っているendDateよりも早いですstartDate

私はその追加したときにしかしperiodに背中をstartDate、私は取得していた結果ではありませんendDateが、日一日以前:

val endDate1 = startDate.plus(period)  // 2019-09-29

だから問題は、なぜ不変ではないのですか

startDate.plus(Period.between(startDate, endDate)) == endDate

これら2つの日付を保持しますか?

Period.between間違った期間を返すのは誰か、それともLocalDate.plus間違って追加するのは誰ですか?


この質問はstackoverflow.com/questions/41945704に似ていますが、完全に同じではないことに注意してください。期間を追加して差し引いた後(date.plus(period).minus(period))、結果は必ずしも同じ日付ではないことを理解しています。この質問は、Period.between関数の不変条件についての詳細です。
Ilya

1
これがjava.time-calendar演算の仕組みです。基本的に追加と削除は互いに会話できません。特に、一方または両方の日付の日が28より大きい場合はそうではありません。数学的な背景については、私のtime lib Time4JのAbstractDurationのクラスドキュメントも参照してください...
メノホックシールド

@MenoHochschild AbstractDurationドキュメントは不変性t1.plus(t1.until(t2)).equals(t2) == trueが保持されるべきであると述べています、そして私はなぜそれがjava.timeここではそうではないのかと尋ねています。
イリヤ

回答:


6

plus実装方法を見るとLocalDate

@Override
public LocalDate plus(TemporalAmount amountToAdd) {
    if (amountToAdd instanceof Period) {
        Period periodToAdd = (Period) amountToAdd;
        return plusMonths(periodToAdd.toTotalMonths()).plusDays(periodToAdd.getDays());
    }
    ...
}

あなたはわかりますplusMonths(...)し、plusDays(...)そこに。

plusMonths1か月が31日、もう1か月が30日のケースを処理します。したがって、次のコードは2019-09-30存在しない代わりに出力されます2019-09-31

println(startDate.plusMonths(period.months.toLong()))

その後、1日を減算するとになり2019-09-29ます。これは正しい結果です。これは、2019-09-292019-10-31が1か月と1日離れているためです

Period.between計算は奇妙であり、この場合にはつまるところ

    LocalDate end = LocalDate.from(endDateExclusive);
    long totalMonths = end.getProlepticMonth() - this.getProlepticMonth();
    int days = end.day - this.day;
    long years = totalMonths / 12;
    int months = (int) (totalMonths % 12);  // safe
    return Period.of(Math.toIntExact(years), months, days);

ここでgetProlepticMonth、00-00-00からの月の総数です。この場合、1か月と1日です。

私の理解から、次のコードは同じ意味を持つため、それはPeriod.betweenLocalDate#plusのネガティブ期間の相互作用のバグです

val startDate = LocalDate.of(2019, 10, 31)
val endDate = LocalDate.of(2019, 9, 30)
val period = Period.between(endDate, startDate)

println(endDate.plus(period))

しかし、それは正しい 2019-10-31

問題は、LocalDate#plusMonths日付を常に「正しい」ように正規化することです。次のコードで2019-10-31は、結果から1か月を引いた後、次のように2019-09-31正規化されていることがわかります。2019-10-30

public LocalDate plusMonths(long monthsToAdd) {
    ...
    return resolvePreviousValid(newYear, newMonth, day);
}

private static LocalDate resolvePreviousValid(int year, int month, int day) {
    switch (month) {
        ...
        case 9:
        case 11:
            day = Math.min(day, 30);
            break;
    }
    return new LocalDate(year, month, day);
}

3

私はあなたが単に運が悪いと信じています。あなたが発明した不変条件は妥当に聞こえますが、java.timeでは保持されません。

のようです betweenメソッドは月の数と月の日を減算するだけの結果には同じ符号が付いているため、この結果は満足のいくものです。私はおそらくもっと良い決断がここで取られたかもしれないと思うが、@ Meno Hochschildが正しく述べたように、29、30、または31か月を含む数学はほとんど明確にできない可能性があり、私はより良いルールが何を持っているかを敢えて示唆しないされています。

彼らは今それを変えるつもりはないに違いない。バグレポートを提出しても(いつでも試すことができます)。コードが多すぎると、すでに5年半以上の動作に依存しています。

追加P-1M-1Dの開始日に戻って、私が期待したように動作します。10月31日から1か月を差し引くと(実際には–1か月に9月30日が加算され)、1日を差し引くと9月29日になります。繰り返しますが、明確ではないため、代わりに9月30日を支持することもできます。


3

期待の分析(疑似コード)

startDate.plus(Period.between(startDate, endDate)) == endDate

いくつかのトピックについて話し合う必要があります。

  • 月や日などの個別の単位をどのように処理しますか?
  • 期間(または「期間」)の追加はどのように定義されますか?
  • 2つの日付間の時間的な距離(期間)を決定する方法
  • 期間(または「期間」)の減算はどのように定義されますか?

最初にユニットを見てみましょう。日は可能な限り最小のカレンダー単位であり、すべてのカレンダー日付は完全な日数で他の日付とは異なるため、問題ありません。したがって、正または負の場合は常に等しい疑似コードを使用します。

startDate.plus(ChronoUnit.Days.between(startDate, endDate)) == endDate

ただし、グレゴリオ暦は異なる長さの暦月を定義するため、月は扱いにくいです。そのため、日付に月の整数を追加すると、日付が無効になる可能性があります。

[2019-08-31] + P1M = [2019-09-31]

java.time終了日を有効な日付(ここでは[2019-09-30])に減らすという決定は合理的であり、最終日でも計算された月が保持されるため、ほとんどのユーザーの期待に対応します。ただし、月末の修正を含むこの追加は元に戻せません。減算と呼ばれる元に戻された演算を参照してください。

[2019-09-30]-P1M = [2019-08-30]

a)月追加の基本的なルールは、月の日をできるだけ多く保つことであり、b)[2019-08-30] + P1M = [2019-09-30]であるため、結果も妥当です。

期間(期間)の正確な追加とは何ですか?

java.timea Periodは、年、月、日と整数の部分的な金額で構成されるアイテムの構成です。したがって、a Periodの追加は、開始日に部分的な金額を追加することで解決できます。年は常に月の12の倍数に変換できるため、うるう年の奇妙な副作用を回避するために、最初に年と月を組み合わせてから、合計を1ステップで追加できます。日は最後のステップで追加できます。で行われたような合理的なデザインjava.time

Period2つの日付の間の権利を判断する方法?

最初に、期間が正の場合、つまり開始日が終了日より前の場合について説明します。次に、最初は月単位で、次に日単位で差を決定することで、常に期間を定義できます。それ以外の場合、2つの日付の間の期間はすべて日のみで構成されるため、この順序は月コンポーネントを実現するために重要です。あなたの例の日付を使う:

[2019-09-30] + P1M1D = [2019-10-31]

技術的には、開始日は最初に、計算された開始と終了の間の月の差によって前方に移動されます。次に、移動された開始日と終了日の差としての日差が、移動された開始日に追加されます。このように、例では期間をP1M1Dとして計算できます。これまでのところ合理的です。

期間を引く方法は?

前の追加の例で最も興味深い点は、誤って月末の修正がないということです。それにもかかわらずjava.time、逆減算を実行できません。最初に月を減算し、次に日を減算します。

[2019-10-31]-P1M1D = [2019-09-29]

java.time代わりに、以前の追加のステップを逆にしようとした場合、自然な選択は、最初に日を減算し、次に月を減算することでした。この変更された注文により、[2019-09-30]が取得されます。減算の変更された順序は、対応する加算ステップで月末の修正がない限り、役立ちます。これは、開始日または終了日の月の日付が28(可能な最小の月の長さ)以下の場合に特に当てはまります。残念なことにjava.time、減算の別の設計が定義されていますが、Periodその結果、一貫性のない結果になります。

減算で期間の加算を元に戻すことはできますか?

最初に、特定のカレンダー日付から期間を減算する際に提案される変更された順序は、加算の可逆性を保証しないことを理解する必要があります。追加で月末の修正があるカウンターの例:

[2011-03-31] + P3M1D = [2011-06-30] + P1D = [2011-07-01] (ok)
[2011-07-01] - P3M1D = [2011-06-30] - P3M = [2011-03-30] :-(

より一貫した結果が得られるため、順序を変更しても問題はありません。しかし、残りの欠陥をどうやって治療するのでしょうか?残された唯一の方法は、継続時間の計算も変更することです。P3M1Dを使用する代わりに、期間P2M31Dが両方向で機能することがわかります。

[2011-03-31] + P2M31D = [2011-05-31] + P31D = [2011-07-01] (ok)
[2011-07-01] - P2M31D = [2011-05-31] - P2M = [2011-03-31] (ok)

つまり、計算された期間の正規化を変更するという考えです。これは、計算された月のデルタの加算が減算ステップで可逆的であるかどうかを確認することで実行できます。つまり、月末の修正の必要性を回避できます。java.time残念ながら、そのようなソリューションは提供されていません。これはバグではありませんが、設計上の制限と見なすことができます。

代替案?

上記のアイデアを展開する可逆メトリックにより、タイムライブラリTime4Jを強化しました。次の例を参照してください。

    PlainDate d1 = PlainDate.of(2011, 3, 31);
    PlainDate d2 = PlainDate.of(2011, 7, 1);

    TimeMetric<CalendarUnit, Duration<CalendarUnit>> metric =
        Duration.inYearsMonthsDays().reversible();
    Duration<CalendarUnit> duration =
        metric.between(d1, d2); // P2M31D
    Duration<CalendarUnit> invDur =
        metric.between(d2, d1); // -P2M31D

    assertThat(d1.plus(duration), is(d2)); // first invariance
    assertThat(invDur, is(duration.inverse())); // second invariance
    assertThat(d2.minus(duration), is(d1)); // third invariance
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.