首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 日期運算實戰:根據上課規則計算課程結束日期的完整算法

     編輯:Java 相關知識

業務場景:課程安排在每週一、二、五上課,每次 1 節,共 N 節。給定開始日期 (通常為今天),計算上完所有課程的具體結束日期。本文提供基於 java.time (JDK 8+) 的現代實現,涵蓋邊界條件處理、可測試設計、擴展性考量。

核心邏輯分析

  • 一週 7 天,上課日固定:MONDAY(1)、TUESDAY(2)、FRIDAY(5) (ISO-8601 週一=1...週日=7)
  • 每週上課 3 天,課程總節數 ÷ 3 = 完整週數,餘數決定最後一週上課天數
  • 起始日期若非上課日,需跳過至下一個上課日

現代實現 (Java 8+ java.time)


import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
import java.util.Arrays;
import java.util.List;

public class CourseScheduleCalculator {

    // 上課日集合 (ISO: MONDAY=1 ... SUNDAY=7)
    private static final List CLASS_DAYS = Arrays.asList(
        DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.FRIDAY
    );
    private static final int CLASSES_PER_WEEK = CLASS_DAYS.size();

    /**
     * 計算課程結束日期
     * @param startDate 開始日期 (含),通常為今天
     * @param totalClasses 總課程節數
     * @return 最後一節課的日期
     */
    public static LocalDate calculateEndDate(LocalDate startDate, int totalClasses) {
        if (totalClasses <= 0) throw new IllegalArgumentException("課程節數必須 > 0");
        
        LocalDate current = startDate;
        int remaining = totalClasses;

        // 1. 對齊到第一個上課日 (若開始日非上課日)
        current = alignToNextClassDay(current);
        
        // 2. 快速跳過完整週
        int fullWeeks = (remaining - 1) / CLASSES_PER_WEEK; // -1 因為當前週可能不完整
        if (fullWeeks > 0) {
            current = current.plusWeeks(fullWeeks);
            remaining -= fullWeeks * CLASSES_PER_WEEK;
        }

        // 3. 逐日推進剩餘課程 (最多 6 天)
        while (remaining > 0) {
            if (isClassDay(current)) {
                remaining--;
                if (remaining == 0) break; // 找到最後一天
            }
            current = current.plusDays(1);
        }
        return current;
    }

    private static boolean isClassDay(LocalDate date) {
        return CLASS_DAYS.contains(date.getDayOfWeek());
    }

    private static LocalDate alignToNextClassDay(LocalDate date) {
        if (isClassDay(date)) return date;
        // 找到下一個上課日
        for (int i = 1; i <= 7; i++) {
            LocalDate next = date.plusDays(i);
            if (isClassDay(next)) return next;
        }
        throw new IllegalStateException("邏輯錯誤:一週內必有上課日");
    }

    // ========== 測試驗證 ==========
    public static void main(String[] args) {
        LocalDate today = LocalDate.now();
        System.out.println("今天: " + today + " (" + today.getDayOfWeek() + ")");
        
        int[] testCases = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 30, 50, 100};
        for (int n : testCases) {
            LocalDate end = calculateEndDate(today, n);
            long daysSpan = java.time.temporal.ChronoUnit.DAYS.between(today, end);
            System.out.printf("課程節數: %3d -> 結束日期: %s (%s)  跨度: %3d 天%n", 
                n, end, end.getDayOfWeek(), daysSpan);
        }
    }
}

算法關鍵點解析

步驟邏輯原因
對齊首個上課日若開始日非週一/二/五,向後查找最近上課日保證從有效上課日開始計數
完整週快速跳躍(remaining-1) / 3 週,plusWeeks()O(1) 時間複雜度,避免逐日循環
餘數逐日推進最多循環 6 次 (一週內)處理最後不完整週,邏輯簡單無邊界錯誤
使用 java.timeLocalDate 不可變、線程安全、時區無關替代舊 Calendar/Date,避免可變性 Bug

擴展:支持自定義上課規則


public class FlexibleScheduleCalculator {
    private final List classDays;
    private final int classesPerWeek;

    public FlexibleScheduleCalculator(DayOfWeek... days) {
        this.classDays = Arrays.asList(days);
        this.classesPerWeek = classDays.size();
    }

    public LocalDate calculateEndDate(LocalDate startDate, int totalClasses) {
        // 邏輯同前,將 CLASS_DAYS 替換為 this.classDays
        // ...
    }
}

// 使用:每週三、四、六上課
FlexibleScheduleCalculator calc = new FlexibleScheduleCalculator(
    DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.SATURDAY
);

舊版 Calendar 實現遷移對比


// 舊代碼痛點:
Calendar cal = Calendar.getInstance(); // 可變、非線程安全
cal.set(Calendar.DAY_OF_WEEK, ...);    // 魔法數字:SUNDAY=1, MONDAY=2...
// 手工 switch-case 計算天數,易出 off-by-one 錯誤
// 無時區概念,跨 DST 邊界錯誤

// 新代碼優勢:
LocalDate date = LocalDate.now();      // 不可變、線程安全
date.getDayOfWeek();                   // 類型安全枚舉
date.plusWeeks(2);                     // 鏈式調用、不可變
// 內建處理閏年、月份長度、ISO 週規則

單元測試建議


@Test void testBoundaryConditions() {
    // 1. 開始日即為上課日
    assertEquals(LocalDate.of(2024,1,8), calc.calculateEndDate(LocalDate.of(2024,1,8), 1)); // 週一
    
    // 2. 開始日非上課日 (週三) -> 跳至週五
    assertEquals(LocalDate.of(2024,1,12), calc.calculateEndDate(LocalDate.of(2024,1,10), 1)); 
    
    // 3. 橫跨週末
    assertEquals(LocalDate.of(2024,1,15), calc.calculateEndDate(LocalDate.of(2024,1,8), 4)); // 週一+3=週一
    
    // 4. 大數值
    assertEquals(LocalDate.of(2024,6,14), calc.calculateEndDate(LocalDate.of(2024,1,8), 50));
}

總結

基於 java.time.LocalDate 實現課程日期計算,通過對齊首日 + 完整週跳躍 + 餘數逐日推進三步法,實現 O(1) 核心邏輯 + O(1) 餘數處理,代碼清晰、無副作用、易測試、易擴展。摒棄舊 Calendar 可變 API,擁抱不可變日期時間模型,是現代 Java 業務日期計算的標準姿勢。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved