Das Hinzufügen von Period zu startDate erzeugt kein endDate


8

Ich habe zwei LocalDates wie folgt deklariert:

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

Dann berechne ich den Zeitraum zwischen ihnen mit der Period.betweenFunktion:

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

Hier hat der Zeitraum die negative Anzahl von Monaten und Tagen, die erwartet wird, da dies endDatefrüher als ist startDate.

Wenn ich das jedoch periodwieder zu dem hinzufüge, startDateist das Ergebnis, das ich erhalte, nicht das endDate, sondern das Datum einen Tag zuvor:

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

Die Frage ist also, warum nicht die Invariante

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

für diese beiden Termine halten?

Gibt Period.betweenjemand einen falschen Punkt zurück oder LocalDate.plusfügt er ihn falsch hinzu?


Beachten Sie, dass diese Frage ähnlich wie stackoverflow.com/questions/41945704 aussieht , jedoch nicht ganz dieselbe ist. Ich verstehe, dass date.plus(period).minus(period)das Ergebnis nach dem Hinzufügen und Subtrahieren von Punkt ( ) nicht immer das gleiche Datum ist. In dieser Frage geht es mehr um Period.betweendie Invarianten der Funktion.
Ilya

1
So funktioniert die java.timeKalenderarithmetik. Grundsätzlich sind Hinzufügen und Entfernen nicht miteinander konvertierbar, insbesondere nicht, wenn der Tag des Monats eines oder beider Daten größer als 28 ist. Weitere mathematische Hintergrundinformationen finden Sie in der Klassendokumentation von AbstractDuration in meiner Zeit lib Time4J ...
Meno Hochschild

@MenoHochschild Die AbstractDurationDokumente besagen, dass die Invarianz gelten t1.plus(t1.until(t2)).equals(t2) == truesollte, und ich frage , warum dies hier nicht der Fall java.timeist.
Ilya

Antworten:


6

Wenn Sie schauen, wie plusfür implementiert istLocalDate

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

du wirst sehen plusMonths(...)und plusDays(...)da.

plusMonthsBehandelt Fälle, in denen ein Monat 31 Tage und der andere 30 Tage hat. Der folgende Code wird also gedruckt, 2019-09-30anstatt nicht vorhanden zu sein2019-09-31

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

Danach führt das Subtrahieren eines Tages zu 2019-09-29. Dies ist das richtige Ergebnis, da 2019-09-29und 2019-10-311 Monat 1 Tag auseinander liegen

Die Period.betweenBerechnung ist seltsam und läuft in diesem Fall auf

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

Wo getProlepticMonthist die Gesamtzahl der Monate von 00-00-00. In diesem Fall ist es 1 Monat und 1 Tag.

Von meinem Verständnis, es ist ein Fehler in einem Period.betweenund LocalDate#plusfür negative Perioden Interaktion, da der folgende Code die gleiche Bedeutung hat ,

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

println(endDate.plus(period))

aber es druckt das richtige 2019-10-31.

Das Problem ist, dass das LocalDate#plusMonthsDatum normalisiert wird, um immer "korrekt" zu sein. Im folgenden Code können Sie sehen, dass nach Subtraktion von 1 Monat vom 2019-10-31Ergebnis 2019-09-31das dann auf normalisiert wird2019-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

Ich glaube, dass Sie einfach kein Glück haben. Die Invariante, die Sie erfunden haben, klingt vernünftig, hält aber nicht in java.time.

Es scheint, dass die between Methode nur die Monatszahlen und die Tage des Monats subtrahiert und mit den Ergebnissen zufrieden ist, da die Ergebnisse das gleiche Vorzeichen haben. Ich glaube, ich stimme zu, dass hier wahrscheinlich eine bessere Entscheidung hätte getroffen werden können, aber wie @Meno Hochschild richtig gesagt hat, kann Mathematik mit den 29, 30 oder 31 Monaten kaum eindeutig sein, und ich wage nicht vorzuschlagen, was die bessere Regel hätte gewesen.

Ich wette, sie werden es jetzt nicht ändern. Nicht einmal, wenn Sie einen Fehlerbericht einreichen (den Sie immer versuchen können). Zu viel Code hängt bereits davon ab, wie er seit mehr als fünfeinhalb Jahren funktioniert.

Das Hinzufügen P-1M-1Dzum Startdatum funktioniert so, wie ich es erwartet hätte. Das Subtrahieren von 1 Monat vom 31. September bis zum 31. September und das Subtrahieren von 1 Tag ergibt den 29. September. Auch hier ist es nicht eindeutig, man könnte stattdessen für den 30. September argumentieren.


3

Analyse Ihrer Erwartungen (im Pseudocode)

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

Wir müssen verschiedene Themen diskutieren:

  • Wie gehe ich mit getrennten Einheiten wie Monaten oder Tagen um?
  • Wie wird die Hinzufügung einer Dauer (oder "Periode") definiert?
  • Wie bestimme ich den zeitlichen Abstand (Dauer) zwischen zwei Daten?
  • Wie ist die Subtraktion einer Dauer (oder "Periode") definiert?

Schauen wir uns zuerst die Einheiten an. Tage sind kein Problem, da sie die kleinstmögliche Kalendereinheit sind und sich jedes Kalenderdatum von jedem anderen Datum in vollen Ganzzahlen von Tagen unterscheidet. Wir haben also im Pseudocode immer gleich, ob positiv oder negativ:

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

Monate sind jedoch schwierig, da der gregorianische Kalender Kalendermonate mit unterschiedlichen Längen definiert. Es kann also vorkommen, dass das Hinzufügen einer Ganzzahl von Monaten zu einem Datum zu einem ungültigen Datum führen kann:

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

Die Entscheidung java.time, das Enddatum auf ein gültiges zu reduzieren - hier [30.09.2019] - ist vernünftig und entspricht den Erwartungen der meisten Benutzer, da das Enddatum den berechneten Monat weiterhin beibehält. Diese Addition einschließlich einer Monatsendkorrektur ist jedoch NICHT umkehrbar , siehe die rückgängig gemachte Operation namens Subtraktion:

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

Das Ergebnis ist auch deshalb vernünftig, weil a) die Grundregel der Monatsaddition darin besteht, den Tag des Monats so weit wie möglich zu halten und b) [2019-08-30] + P1M = [2019-09-30].

Was genau ist die Hinzufügung einer Dauer (Periode)?

In java.timeist a Periodeine Zusammensetzung von Elementen, die aus Jahren, Monaten und Tagen mit ganzzahligen Teilbeträgen besteht. So kann die Hinzufügung von a Periodzur Hinzufügung der Teilbeträge zum Startdatum aufgelöst werden. Da Jahre immer in 12-Vielfache von Monaten konvertierbar sind, können wir zuerst Jahre und Monate kombinieren und dann die Summe in einem Schritt addieren, um seltsame Nebenwirkungen in Schaltjahren zu vermeiden. Die Tage können im letzten Schritt hinzugefügt werden. Ein vernünftiges Design wie in java.time.

Wie kann man das Recht Periodzwischen zwei Daten bestimmen ?

Lassen Sie uns zunächst den Fall diskutieren, in dem die Dauer positiv ist, dh das Startdatum liegt vor dem Enddatum. Dann können wir die Dauer immer definieren, indem wir zuerst die Differenz in Monaten und dann in Tagen bestimmen. Diese Reihenfolge ist wichtig, um eine Monatskomponente zu erreichen, da sonst jede Dauer zwischen zwei Daten nur aus Tagen bestehen würde. Verwenden Sie Ihre Beispieldaten:

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

Technisch gesehen wird das Startdatum zunächst um die berechnete Differenz in Monaten zwischen Start und Ende verschoben. Dann wird das Tagesdelta als Differenz zwischen dem verschobenen Startdatum und dem Enddatum zum verschobenen Startdatum hinzugefügt. Auf diese Weise können wir die Dauer im Beispiel als P1M1D berechnen. So weit so vernünftig.

Wie subtrahiere ich eine Dauer?

Der interessanteste Punkt im vorherigen Additionsbeispiel ist, dass es versehentlich KEINE Monatsendkorrektur gibt. Trotzdem java.timekann die umgekehrte Subtraktion nicht durchgeführt werden. Es subtrahiert zuerst die Monate und dann die Tage:

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

Wenn java.timestattdessen versucht worden wäre, die Schritte in der Addition vorher umzukehren, wäre die natürliche Wahl gewesen, zuerst die Tage und dann die Monate zu subtrahieren . Mit dieser geänderten Reihenfolge würden wir [2019-09-30] erhalten. Die geänderte Reihenfolge in der Subtraktion würde helfen, solange im entsprechenden Additionsschritt keine Korrektur zum Monatsende erfolgt. Dies gilt insbesondere dann, wenn der Tag des Monats eines Start- oder Enddatums nicht größer als 28 ist (die minimal mögliche Monatslänge). Leider java.timewurde ein anderes Design definiert, dessen Subtraktion Periodzu weniger konsistenten Ergebnissen führt.

Ist die Addition einer Dauer in der Subtraktion reversibel?

Zunächst müssen wir verstehen, dass die vorgeschlagene geänderte Reihenfolge bei der Subtraktion einer Dauer von einem bestimmten Kalenderdatum nicht die Umkehrbarkeit der Addition garantiert. Gegenbeispiel mit einer Monatsendkorrektur im Zusatz:

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

Das Ändern der Reihenfolge ist nicht schlecht, da es konsistentere Ergebnisse liefert. Aber wie können die verbleibenden Mängel behoben werden? Die einzige Möglichkeit besteht darin, auch die Berechnung der Dauer zu ändern. Anstatt P3M1D zu verwenden, können wir sehen, dass die Dauer P2M31D in beide Richtungen funktioniert:

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

Die Idee ist also, die Normalisierung der berechneten Dauer zu ändern. Dies kann erreicht werden, indem geprüft wird, ob die Addition des berechneten Monatsdeltas in einem Subtraktionsschritt reversibel ist - dh die Notwendigkeit einer Korrektur zum Monatsende wird vermieden. java.timebietet leider keine solche lösung an. Es ist kein Fehler, kann aber als Designbeschränkung angesehen werden.

Alternativen?

Ich habe meine Zeitbibliothek Time4J um reversible Metriken erweitert, die die oben angegebenen Ideen implementieren. Siehe folgendes Beispiel:

    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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.