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

Java 計算兩日期間工作日(排除周末與法定節假日)工具類實現

     編輯:Java 綜合教學

Java 計算兩日期間工作日(排除周末與法定節假日)工具類實現

售後工單系統常需計算「實際處理工時」,需精確扣除周六日與法定節假日。本文提供一個生產級工具類 GetWorkDay,支持毫秒級精度、多重載入參、自定義節假日列表、結果格式化輸出。

一、核心設計思路

  1. 時間標準化:將輸入時間統一轉為毫秒數(long)計算
  2. 周末修正:起始時間若落在周末,自動順延至下周一 09:00;結束時間若落在周末,自動前推至周五 18:00
  3. 整周快速計算:兩個「下周一」之間的完整周數 × 5 天 × 24h × 3600s × 1000ms
  4. 邊界偏移量:計算起始日剩餘工時與結束日已用工時
  5. 節假日扣除:遍歷自定義節假日列表,扣除範圍內的節假日毫秒數

二、完整工具類代碼

package com.utouu.uphone.commons;

import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

@Component
public class GetWorkDay {

    /** 核心計算邏法:返回工作日毫秒數 */
    private double getWorkdayTimeInMillis(long start, long end, List<Date> holidays) {
        if (start > end) { long t = start; start = end; end = t; }

        Calendar sCal = Calendar.getInstance(); sCal.setTimeInMillis(start);
        Calendar eCal = Calendar.getInstance(); eCal.setTimeInMillis(end);

        // 1. 扣除法定節假日
        long holidayMillis = 0;
        if (holidays != null) {
            holidayMillis = getHolidaysInMillis(start, end, holidays);
            holidays.clear();
        }

        // 2. 同周且非周末:直接返回差值
        if (sameWeekNonWeekend(sCal, eCal)) {
            return end - start - holidayMillis;
        }

        // 3. 同周且皆為周末:返回 0
        if (sameWeekAllWeekend(sCal, eCal)) {
            return 0;
        }

        // 4. 標準化起止時間(修正周末邊界)
        long stdStart = validateStartTime(sCal);
        long stdEnd = validateEndTime(eCal);

        // 5. 計算兩個「下周一」之間的完整工作日毫秒數
        Calendar sMon = getNextMonday((Calendar) sCal.clone());
        Calendar eMon = getNextMonday((Calendar) eCal.clone());
        int weeks = getDaysBetween(sMon, eMon) / 7;
        double fullWeekMillis = weeks * 5.0 * 24 * 3600 * 1000;

        // 6. 邊界偏移量
        double boundaryMillis = calcBoundaryMillis(sCal, eCal, stdStart, stdEnd);

        double result = fullWeekMillis + boundaryMillis - holidayMillis;
        return result > 0 ? result : 0;
    }

    // -------- 輔助方法 --------
    private long validateStartTime(Calendar cal) {
        int dow = cal.get(Calendar.DAY_OF_WEEK);
        if (dow == Calendar.SUNDAY) { // 周日 -> 下周一 09:00
            cal.add(Calendar.DATE, 1);
            return setToTime(cal, 9, 0, 0);
        } else if (dow == Calendar.SATURDAY) { // 周六 -> 下周一 09:00
            cal.add(Calendar.DATE, 2);
            return setToTime(cal, 9, 0, 0);
        }
        return cal.getTimeInMillis();
    }

    private long validateEndTime(Calendar cal) {
        int dow = cal.get(Calendar.DAY_OF_WEEK);
        if (dow == Calendar.SUNDAY || dow == Calendar.SATURDAY) { // 周末 -> 本周五 18:00
            cal.add(Calendar.DATE, dow == Calendar.SUNDAY ? -2 : -1);
            return setToTime(cal, 18, 0, 0);
        }
        return cal.getTimeInMillis();
    }

    private long setToTime(Calendar cal, int hour, int min, int sec) {
        cal.set(Calendar.HOUR_OF_DAY, hour);
        cal.set(Calendar.MINUTE, min);
        cal.set(Calendar.SECOND, sec);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTimeInMillis();
    }

    private Calendar getNextMonday(Calendar cal) {
        int add = 9 - cal.get(Calendar.DAY_OF_WEEK); // 周日=1 -> +1, 周一=2 -> +7...
        if (add == 8) add = 1;
        cal.add(Calendar.DATE, add);
        return cal;
    }

    private int getDaysBetween(Calendar start, Calendar end) {
        if (start.after(end)) { Calendar t = start; start = end; end = t; }
        int days = end.get(Calendar.DAY_OF_YEAR) - start.get(Calendar.DAY_OF_YEAR);
        int y2 = end.get(Calendar.YEAR);
        Calendar tmp = (Calendar) start.clone();
        while (tmp.get(Calendar.YEAR) != y2) {
            days += tmp.getActualMaximum(Calendar.DAY_OF_YEAR);
            tmp.add(Calendar.YEAR, 1);
        }
        return days;
    }

    private long getHolidaysInMillis(long start, long end, List<Date> list) {
        Calendar s = Calendar.getInstance(); s.setTimeInMillis(start);
        Calendar e = Calendar.getInstance(); e.setTimeInMillis(end);
        int count = 0;
        for (Date h : list) {
            Calendar c = Calendar.getInstance(); c.setTime(h);
            if ((c.after(s) && c.before(e)) || c.getTimeInMillis() == s.getTimeInMillis() || c.getTimeInMillis() == e.getTimeInMillis()) {
                count++;
            }
        }
        return count * 24L * 3600 * 1000;
    }

    private double calcBoundaryMillis(Calendar sCal, Calendar eCal, long stdStart, long stdEnd) {
        double startOffset = 0, endOffset = 0;
        if (!isWeekend(sCal)) {
            long todayMillis = sCal.getTimeInMillis();
            long worked = todayMillis - stdStart; // 已工作毫秒
            startOffset = (24*3600*1000) - worked; // 剩餘工時
        }
        if (!isWeekend(eCal)) {
            long todayMillis = eCal.getTimeInMillis();
            long worked = stdEnd - todayMillis; // 剩餘工時
            endOffset = worked;
        }
        return startOffset - endOffset;
    }

    private boolean isWeekend(Calendar cal) {
        int dow = cal.get(Calendar.DAY_OF_WEEK);
        return dow == Calendar.SATURDAY || dow == Calendar.SUNDAY;
    }

    private boolean sameWeekNonWeekend(Calendar s, Calendar e) {
        return s.get(Calendar.YEAR) == e.get(Calendar.YEAR) &&
               s.get(Calendar.WEEK_OF_YEAR) == e.get(Calendar.WEEK_OF_YEAR) &&
               !isWeekend(s) && !isWeekend(e);
    }

    private boolean sameWeekAllWeekend(Calendar s, Calendar e) {
        return s.get(Calendar.YEAR) == e.get(Calendar.YEAR) &&
               s.get(Calendar.WEEK_OF_YEAR) == e.get(Calendar.WEEK_OF_YEAR) &&
               isWeekend(s) && isWeekend(e);
    }

    // -------- 公開重載方法 --------
    public double getWorkdayTimeInMillisExcWeekend(long start, long end) {
        return getWorkdayTimeInMillis(start, end, null);
    }

    public double getWorkdayTimeInMillisExcWeekendHolidays(String start, String end, String fmt, List<Date> holidays) {
        try {
            SimpleDateFormat sdf = new SimpleDateFormat(fmt);
            return getWorkdayTimeInMillis(sdf.parse(start).getTime(), sdf.parse(end).getTime(), holidays);
        } catch (ParseException e) { return 0; }
    }

    public double getWorkdayTimeInMillis(Long s, Long e) { return getWorkdayTimeInMillis(s, e, null); }
    public double getWorkdayTimeInMillis(Date s, Date e) { return getWorkdayTimeInMillis(s.getTime(), e.getTime(), null); }
    public double getWorkdayTimeInMillis(String s, String e, String fmt) {
        try { return getWorkdayTimeInMillis(new SimpleDateFormat(fmt).parse(s).getTime(), new SimpleDateFormat(fmt).parse(e).getTime(), null); }
        catch (ParseException ex) { return 0; }
    }

    // -------- 格式化輸出 --------
    public String formatDuring(double ms) {
        long d = (long)(ms / 86400000);
        long h = (long)((ms % 86400000) / 3600000);
        long m = (long)((ms % 3600000) / 60000);
        long s = (long)((ms % 60000) / 1000);
        return String.format("%d days %d hours %d minutes %d seconds", d, h, m, s);
    }

    public double formateToDay(double ms) { return ms / 86400000; }

    // -------- 2016 年節假日示例(實際項目建議從數據庫/配置中心讀取) --------
    public List<Date> initHoliday() throws ParseException {
        List<Date> list = new ArrayList<>();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String[] days = {
            "2016-01-01","2016-01-02","2016-01-03", // 元旦
            "2016-02-07","2016-02-08","2016-02-09","2016-02-10","2016-02-11","2016-02-12","2016-02-13", // 春節
            "2016-04-02","2016-04-03","2016-04-04", // 清明
            "2016-04-30","2016-05-01","2016-05-02", // 勞動節
            "2016-06-09","2016-06-10","2016-06-11", // 端午
            "2016-09-15","2016-09-16","2016-09-17", // 中秋
            "2016-10-01","2016-10-02","2016-10-03","2016-10-04","2016-10-05","2016-10-06","2016-10-07" // 國慶
        };
        for (String d : days) list.add(sdf.parse(d));
        return list;
    }
}

三、使用示例

@Autowired GetWorkDay workDay;

// 計算 2016-06-30 17:12:53 至 2016-08-30 11:27:50 的工作日
List<Date> holidays = workDay.initHoliday();
double ms = workDay.getWorkdayTimeInMillisExcWeekendHolidays(
    "2016-06-30 17-12-53", "2016-08-30 11-27-50",
    "yyyy-MM-dd HH-mm-ss", holidays);

System.out.println("工作日天數:" + workDay.formateToDay(ms)); // 如 42.3 天
System.out.println("詳細時長:" + workDay.formatDuring(ms));   // 如 42 days 3 hours ...

四、關鍵點說明

場景處理邏輯
起始時間在周末自動順延至下周一 09:00
結束時間在周末自動前推至本周五 18:00
跨年計算getDaysBetween 通過 getActualMaximum(DAY_OF_YEAR) 正確處理閏年
節假日配置建議改為從數據庫/配置中心動態加載,支持多年份
工作制假設默認 09:00-18:00,可通過修改 setToTime 參數調整

五、擴展建議

  • 引入 java.time (JSR-310) 替代 Calendar,代碼更簡潔線程安全
  • 節假日數據接入政府發布 API 或配置中心(Nacos/Apollo)實現動態更新
  • 支持自定義工作日曆(如倒班制、異地節假日差異)
  • 添加單元測試覆蓋邊界場景:跨周、跨月、跨年、純周末區間、含節假日區間
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved