Java 中日期差距的年份計算(實歲計算)

最近處理一個涉及實歲計算的問題,ChatGPT 給我這個程式碼,我好奇了一下裡面年份計算邏輯,所以讀了一下原始碼

int age = Period.between(birthDate, currentDate).getYears();

java.time.Period#between
計算主要是依靠 LocalDate#until

public static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive) {
    return startDateInclusive.until(endDateExclusive);
}

java.time.LocalDate#until(java.time.chrono.ChronoLocalDate)
會先計算月份差距,其中用來相減的月份是透過 LocalDate#getProlepticMonth 取得,getProlepticMonth 邏輯比較簡單,是 年 * 12 + 月 – 1,-1 是因為日期在當月表示還不足一個月

private long getProlepticMonth() {
    return (year * 12L + month - 1);
}

月份差距計算後,會再單獨將日相減,如果月份差距大於 0 且結束日小於開始日,表示有一個月份差距不滿一個月,月份差距需要 -1
(例子:1年1月15日 和 1年2月1日,不能算為有一個月差距)
如果開始日期大於結束日期,月份差距小於 0 且結束日大於開始日,一樣表示有一個月份差距不滿一個月,月份差距負數要加回一
(例子:1年2月15日 和 1年1月20日,不能算為有一個月差距)

最終結果的年份差距就是用月份差距 / 12 取整數

@Override
public Period until(ChronoLocalDate endDateExclusive) {
    LocalDate end = LocalDate.from(endDateExclusive);
    long totalMonths = end.getProlepticMonth() - this.getProlepticMonth();  // safe
    int days = end.day - this.day;
    if (totalMonths > 0 && days < 0) {
        totalMonths--;
        LocalDate calcDate = this.plusMonths(totalMonths);
        days = (int) (end.toEpochDay() - calcDate.toEpochDay());  // safe
    } else if (totalMonths < 0 && days > 0) {
        totalMonths++;
        days -= end.lengthOfMonth();
    }
    long years = totalMonths / 12;  // safe
    int months = (int) (totalMonths % 12);  // safe
    return Period.of(Math.toIntExact(years), months, days);
}

已上年份的部分就結束了,但這裡日差距的算法也滿有趣,月份差距大於 0 且結束日小於開始日,會將開始日期加上月份差距去計算日差距

例:1年1月15日 和 1年2月1日,「1年1月15日」+0月還是「1年1月15日」,計算日差距為 17

例:1年1月15日 和 1年3月1日,「1年1月15日」+1月是「1年2月15日」,計算日差距為 14(28-15+1)

public LocalDate plusMonths(long monthsToAdd) {
    if (monthsToAdd == 0) {
        return this;
    }
    long monthCount = year * 12L + (month - 1);
    long calcMonths = monthCount + monthsToAdd;  // safe overflow
    int newYear = YEAR.checkValidIntValue(Math.floorDiv(calcMonths, 12));
    int newMonth = Math.floorMod(calcMonths, 12) + 1;
    return resolvePreviousValid(newYear, newMonth, day);
}

結論

JDK 的 method 計算邏輯符合我們一般認知的,到了生日那天就是滿 X 歲,如果是網路上有一種用毫秒計算,然後一年以 365 天計算,遇到年份中有跨到閏年,一定會有誤差。