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

Java 計算任意兩日期間工作天數的高效演算法實作

     編輯:Java 程式設計入門

問題定義

給定起始日期 dateStart 與結束日期 dateEnd (含雙端),計算兩日期間 **不含週六、週日** 的工作天數。不考慮國定假日,跨年、跨世紀皆需正確處理。

核心思路:以「下一個週一」為基準對齊

  1. 計算兩日期間的總天數 totalDays。
  2. 分別找出 dateStart 與 dateEnd 之後的 **第一個週一**,記為 monStart、monEnd。
  3. monStart 到 monEnd 之間構成完整週,天數差必為 7 的倍數。完整週數 fullWeeks = daysBetween(monStart, monEnd) / 7,對應工作天數 baseWorkDays = fullWeeks * 5。
  4. 修正兩端偏移量:
    • startOffset:dateStart 到 monStart 之間的工作天數 (需加回)。
    • endOffset:monEnd 到 dateEnd 之間的工作天數 (需扣除)。
  5. 結果:baseWorkDays + startOffset - endOffset。

完整實作 (JDK 8+ 建議改用 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() 副本上進行,避免副作用。

現代化替代方案 (JDK 8+)

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 不可變、執行緒安全、語義清晰,新專案強烈建議採用。

擴充建議

  • 引入假日表 (資料庫/快取),於迴圈中排除法定假日。
  • 支援自訂工作週 (如中東地區週五、週六休息)。
  • 單元測試覆蓋:同一天、跨週、跨月、跨年、閏年、起迄互換。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved