給定起始日期 dateStart 與結束日期 dateEnd (含雙端),計算兩日期間 **不含週六、週日** 的工作天數。不考慮國定假日,跨年、跨世紀皆需正確處理。
totalDays。dateStart 與 dateEnd 之後的 **第一個週一**,記為 monStart、monEnd。monStart 到 monEnd 之間構成完整週,天數差必為 7 的倍數。完整週數 fullWeeks = daysBetween(monStart, monEnd) / 7,對應工作天數 baseWorkDays = fullWeeks * 5。startOffset:dateStart 到 monStart 之間的工作天數 (需加回)。endOffset:monEnd 到 dateEnd 之間的工作天數 (需扣除)。baseWorkDays + startOffset - endOffset。java.time,此處保留原 Calendar 風格並修正潛在 Bug)package com.date.util;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class WorkingDayCalculator {
/** 計算兩 Calendar 之間的天數差 (d2 - d1),支援跨年 */
public static int daysBetween(Calendar d1, Calendar d2) {
if (d1.after(d2)) { Calendar tmp = d1; d1 = d2; d2 = tmp; }
int days = d2.get(Calendar.DAY_OF_YEAR) - d1.get(Calendar.DAY_OF_YEAR);
int y2 = d2.get(Calendar.YEAR);
if (d1.get(Calendar.YEAR) != y2) {
d1 = (Calendar) d1.clone();
do {
days += d1.getActualMaximum(Calendar.DAY_OF_YEAR);
d1.add(Calendar.YEAR, 1);
} while (d1.get(Calendar.YEAR) != y2);
}
return days;
}
/** 取得日期之後的第一個週一 (Calendar.MONDAY == 2) */
public static Calendar nextMonday(Calendar date) {
Calendar c = (Calendar) date.clone();
do { c.add(Calendar.DATE, 1); } while (c.get(Calendar.DAY_OF_WEEK) != Calendar.MONDAY);
return c;
}
/** 計算工作天數 (含雙端) */
public static int getWorkingDays(Calendar start, Calendar end) {
if (start.after(end)) { Calendar tmp = start; start = end; end = tmp; }
Calendar monStart = nextMonday(start);
Calendar monEnd = nextMonday(end);
int fullWeeks = daysBetween(monStart, monEnd) / 7;
int baseWorkDays = fullWeeks * 5;
// 起始偏移:start 到 monStart 之間的工作天
int startOffset = 0;
Calendar cursor = (Calendar) start.clone();
while (cursor.before(monStart)) {
int dow = cursor.get(Calendar.DAY_OF_WEEK);
if (dow != Calendar.SATURDAY && dow != Calendar.SUNDAY) startOffset++;
cursor.add(Calendar.DATE, 1);
}
// 結束偏移:monEnd 到 end 之間的工作天
int endOffset = 0;
cursor = (Calendar) monEnd.clone();
while (!cursor.after(end)) {
int dow = cursor.get(Calendar.DAY_OF_WEEK);
if (dow != Calendar.SATURDAY && dow != Calendar.SUNDAY) endOffset++;
cursor.add(Calendar.DATE, 1);
}
return baseWorkDays + startOffset - endOffset;
}
/** 計算休息天數 (含雙端) */
public static int getHolidays(Calendar start, Calendar end) {
return daysBetween(start, end) + 1 - getWorkingDays(start, end);
}
// ----- Demo -----
public static void main(String[] args) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Calendar c1 = Calendar.getInstance(); c1.setTime(sdf.parse("2006-10-01"));
Calendar c2 = Calendar.getInstance(); c2.setTime(sdf.parse("2006-10-14"));
System.out.println("起始:" + sdf.format(c1.getTime()) + " 結束:" + sdf.format(c2.getTime()));
System.out.println("工作天數:" + getWorkingDays(c1, c2)); // 預期 10
System.out.println("休息天數:" + getHolidays(c1, c2)); // 預期 4
}
}
charge_start_date 邏輯有誤:以 7 - DAY_OF_WEEK 推算偏移,在週日(1)會得到 6、週六(7)得到 0,導致週日被誤判為 5 個工作天。改用逐日迴圈判斷 DAY_OF_WEEK != SATURDAY && != SUNDAY,簡單且絕對正確。daysBetween 回傳不含端點的差值,getWorkingDays 以「含雙端」為契約,測試案例 10/1~10/14 共 14 天,工作天 10、休息天 4,符合預期。Calendar 操作均在 clone() 副本上進行,避免副作用。import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public static long workingDays(LocalDate start, LocalDate end) {
if (start.isAfter(end)) { LocalDate t = start; start = end; end = t; }
long days = ChronoUnit.DAYS.between(start, end) + 1; // 含雙端
long fullWeeks = days / 7;
long rem = days % 7;
long work = fullWeeks * 5;
LocalDate cur = start.plusDays(fullWeeks * 7);
for (long i = 0; i < rem; i++, cur = cur.plusDays(1)) {
if (cur.getDayOfWeek() != DayOfWeek.SATURDAY && cur.getDayOfWeek() != DayOfWeek.SUNDAY) work++;
}
return work;
}
java.time API 不可變、執行緒安全、語義清晰,新專案強烈建議採用。