最近處理一個涉及實歲計算的問題,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 天計算,遇到年份中有跨到閏年,一定會有誤差。