計算日期天數差看似簡單,實則暗藏時區、夏令時、毫秒精度等陷阱。本文分析常見錯誤,提供基於 Calendar 歸零時間與 Java 8 LocalDate 兩種標準實現,確保跨時區、跨夏令時正確計算。
// 錯誤 1:直接用毫秒差除以一天毫秒數
long diff = date2.getTime() - date1.getTime();
int days = (int) (diff / (1000 * 60 * 60 * 24));
// 問題:夏令時切換日非 24 小時,時區偏移導致誤差
// 錯誤 2:SimpleDateFormat 解析後直接相減
// 同樣受時區、夏令時影響
import java.util.Calendar;
import java.util.Date;
public class DateDiffUtils {
/**
* 計算兩個日期之間的天數差(date1 - date2)
* 忽略時間部分,僅比較年月日
*/
public static int daysBetween(Date date1, Date date2) {
Calendar cal1 = Calendar.getInstance();
Calendar cal2 = Calendar.getInstance();
cal1.setTime(date1);
cal2.setTime(date2);
// 關鍵:將時間歸零到當天 00:00:00.000
setTimeToMidnight(cal1);
setTimeToMidnight(cal2);
long ms1 = cal1.getTimeInMillis();
long ms2 = cal2.getTimeInMillis();
long diffMs = ms1 - ms2;
return millisecondsToDays(diffMs);
}
private static int millisecondsToDays(long ms) {
return (int) (ms / (1000L * 60 * 60 * 24));
}
private static void setTimeToMidnight(Calendar cal) {
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
}
}
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
public class DateDiffUtils {
/**
* 計算天數差(Java 8+ 推薦)
* @param date1 日期 1
* @param date2 日期 2
* @param zoneId 時區(如 ZoneId.systemDefault())
* @return date1 到 date2 的天數差
*/
public static long daysBetween(Date date1, Date date2, ZoneId zoneId) {
LocalDate ld1 = date1.toInstant().atZone(zoneId).toLocalDate();
LocalDate ld2 = date2.toInstant().atZone(zoneId).toLocalDate();
return java.time.temporal.ChronoUnit.DAYS.between(ld2, ld1);
}
// 重載:使用系統預設時區
public static long daysBetween(Date date1, Date date2) {
return daysBetween(date1, date2, ZoneId.systemDefault());
}
// 直接接受 LocalDate
public static long daysBetween(LocalDate date1, LocalDate date2) {
return ChronoUnit.DAYS.between(date2, date1);
}
}
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
public class Demo {
public static void main(String[] args) {
// 構造測試日期
LocalDate d1 = LocalDate.of(2024, 1, 15);
LocalDate d2 = LocalDate.of(2024, 1, 1);
System.out.println("LocalDate 直接計算: " +
DateDiffUtils.daysBetween(d1, d2)); // 14
// Date 對象轉換
Date date1 = Date.from(d1.atStartOfDay(ZoneId.systemDefault()).toInstant());
Date date2 = Date.from(d2.atStartOfDay(ZoneId.systemDefault()).toInstant());
System.out.println("Date 計算 (Calendar): " +
DateDiffUtils.daysBetween(date1, date2)); // 14
System.out.println("Date 計算 (LocalDate): " +
DateDiffUtils.daysBetween(date1, date2)); // 14
}
}
| 特性 | Calendar 歸零法 | LocalDate (Java 8+) |
|---|---|---|
| API 設計 | 舊式、可變、易錯 | 新式、不可變、線程安全 |
| 時區處理 | 隱式使用系統預設 | 顯式指定 ZoneId |
| 夏令時正確性 | 歸零後通常正確 | 內建正確處理 |
| 可讀性 | 較差 | 極佳 |
| 效能 | 一般 | 更好 |
// 差值定義:date1 - date2
// 2024-01-01 到 2024-01-02 = 1 天
// 2024-01-01 到 2024-01-01 = 0 天
// 若需「包含兩端」天數(如住宿天數):
long inclusiveDays = daysBetween(date1, date2) + 1;
// 紐約時間 2024-03-10 23:00 (夏令時前)
// 倫敦時間 2024-03-11 04:00 (同一時刻)
// 若用各自本地日期計算天數差可能不同
// 解決:統一指定時區或使用 UTC
long days = daysBetween(date1, date2, ZoneId.of("UTC"));
// Joda-Time 寫法
import org.joda.time.Days;
import org.joda.time.LocalDate;
int days = Days.daysBetween(new LocalDate(date1), new LocalDate(date2)).getDays();
java.time.LocalDate + ChronoUnit.DAYS.between()Calendar 歸零時間法,注意設定 MILLISECOND正確處理日期天數差,是避免計費、統計、排程等核心業務出錯的關鍵基礎。