Thêm dấu chấm vào startDate không tạo ra endDate


8

Tôi có hai LocalDates được khai báo như sau:

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

Sau đó, tôi tính khoảng thời gian giữa chúng bằng cách sử dụng Period.betweenhàm:

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

Ở đây, khoảng thời gian có số tháng và ngày âm, dự kiến ​​được đưa ra endDatesớm hơn startDate.

Tuy nhiên, khi tôi thêm nó periodtrở lại startDate, kết quả tôi nhận được không phải là endDate, mà là ngày trước đó một ngày:

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

Vì vậy, câu hỏi là, tại sao không bất biến

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

giữ cho hai ngày này?

Period.betweenai trả lại một khoảng thời gian không chính xác, hoặc LocalDate.plusai thêm nó không đúng?


Lưu ý rằng câu hỏi này trông tương tự như stackoverflow.com/questions/41945704 , tuy nhiên nó không hoàn toàn giống nhau. Tôi hiểu rằng sau khi thêm thời gian và trừ nó trở lại ( date.plus(period).minus(period)) kết quả không phải lúc nào cũng giống nhau. Câu hỏi này là nhiều hơn về Period.betweenbất biến của chức năng.
Ilya

1
Đó là cách java.timehoạt động của số học. Về cơ bản, việc thêm và xóa không thể đối thoại với nhau, đặc biệt là nếu ngày của một hoặc cả hai ngày lớn hơn 28. Xem thêm tài liệu lớp của AbstractDuration trong thời gian của tôi, Time4J để biết thêm về nền toán học ...
Meno Hochschild

@MenoHochschild Các AbstractDurationtài liệu nói rằng sự bất biến t1.plus(t1.until(t2)).equals(t2) == truenên giữ và tôi đang hỏi tại sao không phải như vậy java.timeở đây.
Ilya

Câu trả lời:


6

Nếu bạn nhìn cách plusthực hiện choLocalDate

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

bạn sẽ thấy plusMonths(...)plusDays(...)ở đó.

plusMonthsxử lý các trường hợp khi một tháng có 31 ngày và trường hợp khác có 30. Vì vậy, đoạn mã sau sẽ in 2019-09-30thay vì không tồn tại2019-09-31

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

Sau đó, trừ đi một ngày kết quả 2019-09-29. Đây là kết quả chính xác, vì 2019-09-29và cách 2019-10-31nhau 1 tháng 1 ngày

Tính Period.betweentoán là lạ và trong trường hợp này sôi xuống

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

trong đó getProlepticMonthtổng số tháng từ 00-00-00. Trong trường hợp này, đó là 1 tháng và 1 ngày.

Theo hiểu biết của tôi, đó là một lỗi trong tương tác Period.betweenLocalDate#plustrong giai đoạn tiêu cực, vì đoạn mã sau có cùng ý nghĩa

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

println(endDate.plus(period))

nhưng nó in đúng 2019-10-31.

Vấn đề là LocalDate#plusMonthsngày bình thường luôn luôn "chính xác". Trong đoạn mã sau, bạn có thể thấy rằng sau khi trừ đi 1 tháng từ 2019-10-31kết quả 2019-09-31là sau đó được chuẩn hóa thành2019-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

Tôi tin rằng bạn chỉ đơn giản là hết may mắn. Bất biến mà bạn đã phát minh ra âm thanh hợp lý, nhưng không giữ trong java.time.

Có vẻ như betweenphương pháp chỉ trừ các số tháng và các ngày trong tháng và vì các kết quả có cùng dấu, là nội dung với kết quả này. Tôi nghĩ rằng tôi đồng ý rằng có lẽ một quyết định tốt hơn có thể đã được đưa ra ở đây, nhưng như @Meno Hochschild đã tuyên bố chính xác, toán học liên quan đến 29, 30 hoặc 31 tháng khó có thể rõ ràng và tôi không dám đề xuất quy tắc nào tốt hơn đã.

Tôi cá là họ sẽ không thay đổi nó bây giờ. Ngay cả khi bạn nộp báo cáo lỗi (mà bạn luôn có thể thử). Quá nhiều mã đã dựa vào cách nó hoạt động trong hơn năm năm rưỡi.

Thêm P-1M-1Dlại vào ngày bắt đầu hoạt động theo cách tôi mong đợi. Trừ đi 1 tháng kể từ (thực sự cộng thêm1 tháng đến) ngày 31 tháng 10 vào ngày 30 tháng 9 và trừ đi 1 ngày mang lại ngày 29 tháng 9. Thay vào đó, không rõ ràng, bạn có thể tranh luận ủng hộ ngày 30 tháng 9.


3

Phân tích kỳ vọng của bạn (bằng mã giả)

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

chúng ta phải thảo luận về một số chủ đề:

  • Làm thế nào để xử lý các đơn vị riêng biệt như tháng hoặc ngày?
  • việc thêm thời lượng (hay "thời gian") được định nghĩa như thế nào?
  • Làm thế nào để xác định khoảng cách thời gian (thời lượng) giữa hai ngày?
  • phép trừ của một khoảng thời gian (hoặc "thời gian") được định nghĩa như thế nào?

Trước tiên hãy nhìn vào các đơn vị. Ngày không có vấn đề gì vì chúng là đơn vị lịch nhỏ nhất có thể và mỗi ngày theo lịch khác nhau bởi bất kỳ ngày nào khác trong số nguyên đầy đủ của ngày. Vì vậy, chúng tôi luôn có mã giả bằng nhau nếu dương hoặc âm:

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

Tuy nhiên, các tháng rất khó khăn vì lịch gregorian định nghĩa các tháng theo lịch với các độ dài khác nhau. Vì vậy, tình huống có thể phát sinh là việc thêm bất kỳ số nguyên tháng nào vào một ngày có thể gây ra một ngày không hợp lệ:

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

Quyết định java.timegiảm ngày kết thúc thành hợp lệ - ở đây [2019-09-30] - là hợp lý và tương ứng với mong đợi của hầu hết người dùng vì ngày cuối cùng vẫn giữ nguyên tháng tính toán. Tuy nhiên, phần bổ sung này bao gồm cả hiệu chỉnh cuối tháng KHÔNG thể đảo ngược , hãy xem thao tác hoàn nguyên được gọi là phép trừ:

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

Kết quả cũng hợp lý vì a) quy tắc cơ bản của việc thêm tháng là giữ cho ngày tháng càng nhiều càng tốt và b) [2019-08-30] + P1M = [2019-09-30].

Việc thêm một khoảng thời gian (thời gian) chính xác là gì?

Trong java.time, a Periodlà một thành phần của các vật phẩm bao gồm năm, tháng và ngày với bất kỳ số lượng một phần nguyên nào. Vì vậy, việc thêm một Periodcó thể được giải quyết để thêm số tiền một phần vào ngày bắt đầu. Vì năm luôn có thể chuyển đổi thành 12 bội số của tháng, trước tiên chúng ta có thể kết hợp năm và tháng và sau đó thêm tổng số trong một bước để tránh các tác dụng phụ lạ trong năm nhuận. Những ngày có thể được thêm vào trong bước cuối cùng. Một thiết kế hợp lý như được thực hiện trong java.time.

Làm thế nào để xác định quyền Periodgiữa hai ngày?

Trước tiên chúng ta hãy thảo luận về trường hợp khi thời lượng là dương, có nghĩa là ngày bắt đầu trước ngày kết thúc. Sau đó, chúng ta luôn có thể xác định thời lượng bằng cách trước tiên xác định sự khác biệt theo tháng và sau đó là ngày. Thứ tự này rất quan trọng để đạt được thành phần tháng vì nếu không, mọi khoảng thời gian giữa hai ngày sẽ chỉ bao gồm các ngày. Sử dụng ngày mẫu của bạn:

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

Về mặt kỹ thuật, ngày bắt đầu trước tiên được chuyển tiếp bằng chênh lệch được tính theo tháng giữa ngày bắt đầu và ngày kết thúc. Sau đó, ngày delta là chênh lệch giữa ngày bắt đầu di chuyển và ngày kết thúc được thêm vào ngày bắt đầu di chuyển. Bằng cách này, chúng ta có thể tính thời lượng là P1M1D trong ví dụ. Cho đến nay rất hợp lý.

Làm thế nào để trừ một khoảng thời gian?

Điểm thú vị nhất trong ví dụ bổ sung trước đó là, tình cờ KHÔNG có sự điều chỉnh vào cuối tháng. Tuy nhiên java.timekhông thực hiện phép trừ ngược. Đầu tiên nó trừ đi các tháng và sau đó là các ngày:

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

java.timeThay vào đó, nếu đã cố gắng đảo ngược các bước trong phần bổ sung trước đó thì sự lựa chọn tự nhiên sẽ là đầu tiên trừ đi các ngày và sau đó là các tháng . Với thứ tự thay đổi này, chúng tôi sẽ nhận được [2019-09-30]. Thứ tự thay đổi trong phép trừ sẽ giúp miễn là không có sự điều chỉnh cuối tháng trong bước bổ sung tương ứng. Điều này đặc biệt đúng nếu ngày trong tháng của bất kỳ ngày bắt đầu hoặc ngày kết thúc nào không lớn hơn 28 (độ dài tháng tối thiểu có thể). Thật không may java.timeđã xác định một thiết kế khác cho phép trừ Perioddẫn đến kết quả ít nhất quán hơn.

Là sự bổ sung của một thời lượng có thể đảo ngược trong phép trừ?

Trước tiên, chúng ta phải hiểu rằng thứ tự thay đổi được đề xuất trong phép trừ thời lượng từ một ngày theo lịch nhất định không đảm bảo tính thuận nghịch của phép cộng. Ví dụ về bộ đếm có hiệu chỉnh cuối tháng bổ sung:

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

Thay đổi thứ tự không phải là xấu vì nó mang lại kết quả phù hợp hơn. Nhưng làm thế nào để chữa những thiếu sót còn lại? Cách duy nhất còn lại là thay đổi cách tính thời lượng. Thay vì sử dụng P3M1D, chúng ta có thể thấy rằng thời lượng P2M31D sẽ hoạt động theo cả hai hướng:

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

Vì vậy, ý tưởng là để thay đổi chuẩn hóa của thời lượng tính toán. Điều này có thể được thực hiện bằng cách xem xét việc bổ sung đồng bằng tháng được tính toán có thể đảo ngược trong bước trừ - tức là tránh sự cần thiết phải điều chỉnh vào cuối tháng. java.timekhông may không cung cấp một giải pháp như vậy. Nó không phải là một lỗi, nhưng có thể được coi là một giới hạn thiết kế.

Lựa chọn thay thế?

Tôi đã tăng cường thư viện thời gian Time4J của mình bằng các số liệu có thể đảo ngược, triển khai các ý tưởng được đưa ra ở trên. Xem ví dụ sau:

    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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.