業務場景:課程安排在每週一、二、五上課,每次 1 節,共 N 節。給定開始日期 (通常為今天),計算上完所有課程的具體結束日期。本文提供基於 java.time (JDK 8+) 的現代實現,涵蓋邊界條件處理、可測試設計、擴展性考量。
MONDAY(1)、TUESDAY(2)、FRIDAY(5) (ISO-8601 週一=1...週日=7)
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.time | LocalDate 不可變、線程安全、時區無關 | 替代舊 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 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 業務日期計算的標準姿勢。