Java Period

Period 是 Java 8 引入的類別,用於表示兩個日期之間的間隔,以年、月、日為單位。適合處理基於日期的間隔。

引入套件

import java.time.Period;

建立 Period

使用 of 方法

// 指定年數
Period years = Period.ofYears(2);      // P2Y

// 指定月數
Period months = Period.ofMonths(6);    // P6M

// 指定天數
Period days = Period.ofDays(15);       // P15D

// 指定週數(轉換為天數)
Period weeks = Period.ofWeeks(2);      // P14D

// 指定年、月、日
Period period = Period.of(1, 6, 15);   // P1Y6M15D

從字串解析

// ISO-8601 格式:P[n]Y[n]M[n]D
Period p1 = Period.parse("P1Y");         // 1年
Period p2 = Period.parse("P6M");         // 6個月
Period p3 = Period.parse("P1Y6M15D");    // 1年6個月15天
Period p4 = Period.parse("P2W");         // 2週(14天)

計算兩個日期的間隔

LocalDate start = LocalDate.of(2024, 1, 1);
LocalDate end = LocalDate.of(2024, 12, 31);

Period period = Period.between(start, end);
System.out.println(period);  // P11M30D

// 注意:between 計算的是完整的年月日差
LocalDate birth = LocalDate.of(1990, 5, 15);
LocalDate today = LocalDate.of(2024, 12, 10);
Period age = Period.between(birth, today);
System.out.println(age);  // P34Y6M25D

取得 Period 資訊

Period period = Period.of(2, 6, 15);

// 取得各部分的值
int years = period.getYears();   // 2
int months = period.getMonths(); // 6
int days = period.getDays();     // 15

// 取得總月數
long totalMonths = period.toTotalMonths();  // 30

// 檢查是否為零
boolean isZero = period.isZero();       // false

// 檢查是否為負數
boolean isNegative = period.isNegative(); // false

// 取得所有單位
List<TemporalUnit> units = period.getUnits();
// [Years, Months, Days]

Period 運算

加減運算

Period p1 = Period.of(1, 6, 0);
Period p2 = Period.of(0, 3, 15);

// 加法
Period sum = p1.plus(p2);           // P1Y9M15D
Period plusYears = p1.plusYears(1); // P2Y6M
Period plusMonths = p1.plusMonths(2); // P1Y8M
Period plusDays = p1.plusDays(10);  // P1Y6M10D

// 減法
Period diff = p1.minus(p2);          // P1Y2M-15D
Period minusYears = p1.minusYears(1); // P6M
Period minusMonths = p1.minusMonths(3); // P1Y3M

乘法和取反

Period p = Period.of(1, 2, 3);

// 乘法
Period doubled = p.multipliedBy(2);  // P2Y4M6D

// 取反
Period negated = p.negated();  // P-1Y-2M-3D

正規化

// 將月份超過12的部分轉換為年
Period p = Period.of(1, 15, 10);
Period normalized = p.normalized();
System.out.println(normalized);  // P2Y3M10D

// 注意:天數不會被正規化
Period p2 = Period.of(0, 0, 45);
Period normalized2 = p2.normalized();
System.out.println(normalized2);  // P45D(天數不變)

應用於日期運算

LocalDate date = LocalDate.of(2024, 1, 15);
Period period = Period.of(1, 2, 10);

// 加上 Period
LocalDate later = date.plus(period);  // 2025-03-25

// 減去 Period
LocalDate earlier = date.minus(period);  // 2022-11-05

// 分別加減
LocalDate plusYears = date.plusYears(1);    // 2025-01-15
LocalDate plusMonths = date.plusMonths(2);  // 2024-03-15
LocalDate plusDays = date.plusDays(10);     // 2024-01-25

實用範例

計算年齡

public static String calculateAge(LocalDate birthDate) {
    LocalDate today = LocalDate.now();
    Period age = Period.between(birthDate, today);
    
    return String.format("%d 歲 %d 個月 %d 天",
        age.getYears(),
        age.getMonths(),
        age.getDays()
    );
}

LocalDate birthday = LocalDate.of(1990, 5, 15);
System.out.println("年齡: " + calculateAge(birthday));
// 年齡: 34 歲 6 個月 25 天

計算到期日

public static LocalDate calculateExpiryDate(LocalDate issueDate, Period validity) {
    return issueDate.plus(validity);
}

LocalDate issued = LocalDate.of(2024, 1, 1);
Period validity = Period.of(1, 0, 0);  // 1年有效期

LocalDate expiry = calculateExpiryDate(issued, validity);
System.out.println("到期日: " + expiry);  // 2025-01-01

訂閱週期計算

public class Subscription {
    private LocalDate startDate;
    private Period billingPeriod;
    
    public Subscription(LocalDate startDate, Period billingPeriod) {
        this.startDate = startDate;
        this.billingPeriod = billingPeriod;
    }
    
    public LocalDate getNextBillingDate() {
        LocalDate today = LocalDate.now();
        LocalDate nextBilling = startDate;
        
        while (!nextBilling.isAfter(today)) {
            nextBilling = nextBilling.plus(billingPeriod);
        }
        return nextBilling;
    }
    
    public int getBillingCycleNumber() {
        LocalDate today = LocalDate.now();
        int cycle = 0;
        LocalDate date = startDate;
        
        while (!date.isAfter(today)) {
            date = date.plus(billingPeriod);
            cycle++;
        }
        return cycle;
    }
}

// 月訂閱
Subscription monthly = new Subscription(
    LocalDate.of(2024, 1, 1),
    Period.ofMonths(1)
);
System.out.println("下次扣款日: " + monthly.getNextBillingDate());
System.out.println("目前週期: " + monthly.getBillingCycleNumber());

格式化顯示

public static String formatPeriod(Period period) {
    StringBuilder sb = new StringBuilder();
    
    if (period.getYears() != 0) {
        sb.append(Math.abs(period.getYears())).append(" 年 ");
    }
    if (period.getMonths() != 0) {
        sb.append(Math.abs(period.getMonths())).append(" 個月 ");
    }
    if (period.getDays() != 0) {
        sb.append(Math.abs(period.getDays())).append(" 天");
    }
    
    if (sb.length() == 0) {
        return "0 天";
    }
    
    return sb.toString().trim();
}

Period p = Period.of(2, 3, 15);
System.out.println(formatPeriod(p));  // 2 年 3 個月 15 天

比較兩個日期差距

public static String compareDates(LocalDate date1, LocalDate date2) {
    Period period = Period.between(date1, date2);
    long totalMonths = period.toTotalMonths();
    
    if (period.isZero()) {
        return "相同日期";
    } else if (period.isNegative()) {
        Period abs = period.negated();
        return date1 + " 比 " + date2 + " 晚 " + formatPeriod(abs);
    } else {
        return date1 + " 比 " + date2 + " 早 " + formatPeriod(period);
    }
}

LocalDate d1 = LocalDate.of(2024, 1, 1);
LocalDate d2 = LocalDate.of(2024, 6, 15);
System.out.println(compareDates(d1, d2));
// 2024-01-01 比 2024-06-15 早 5 個月 14 天

專案時程計算

public class Project {
    private String name;
    private LocalDate startDate;
    private Period duration;
    
    public Project(String name, LocalDate startDate, Period duration) {
        this.name = name;
        this.startDate = startDate;
        this.duration = duration;
    }
    
    public LocalDate getEndDate() {
        return startDate.plus(duration);
    }
    
    public Period getRemainingTime() {
        LocalDate today = LocalDate.now();
        LocalDate endDate = getEndDate();
        
        if (today.isAfter(endDate)) {
            return Period.ZERO;
        }
        return Period.between(today, endDate);
    }
    
    public double getProgressPercentage() {
        LocalDate today = LocalDate.now();
        long totalDays = ChronoUnit.DAYS.between(startDate, getEndDate());
        long elapsedDays = ChronoUnit.DAYS.between(startDate, today);
        
        if (elapsedDays >= totalDays) return 100.0;
        if (elapsedDays <= 0) return 0.0;
        
        return (elapsedDays * 100.0) / totalDays;
    }
}

Project project = new Project(
    "系統開發",
    LocalDate.of(2024, 1, 1),
    Period.of(0, 6, 0)  // 6個月
);

System.out.println("專案: " + project.name);
System.out.println("結束日期: " + project.getEndDate());
System.out.println("剩餘時間: " + formatPeriod(project.getRemainingTime()));
System.out.printf("進度: %.1f%%%n", project.getProgressPercentage());

Period vs Duration

特性PeriodDuration
單位年、月、日秒、奈秒
精度天級奈秒級
用途日期間隔時間間隔
適用類型LocalDateLocalTime, Instant
月份處理考慮月份天數差異固定秒數

重點整理

  • Period 表示基於日期的間隔(年、月、日)
  • 使用 ofXxx() 方法建立各種日期長度
  • 使用 between() 計算兩個日期的間隔
  • normalized() 可將月份超過 12 的部分轉為年
  • 不可變(immutable)類別
  • 適合用於 LocalDateLocalDateTime
  • 時間間隔(時分秒)應使用 Duration