Java工作日計算工具類


工作日計算工具類

主要功能:傳入兩個日期,返回這兩個日期之間有多少個工作日。

思路:

  1. 預先設置好一定年份范圍內的節假日、補休到map里。(這里暫時只設置了2017 - 2018年的)
  2. 將這個年份范圍內的每一天是否為節假日存到數組里,以2017-2018為例,兩年有365*2=730天,則開一個數組boolean workdays[730],用於存放這個年份范圍內的每一天是否為工作日。判斷方法為:遍歷這個年份范圍內的每一天,先從map里找是否為節假日或補休,找不到則以是否為周末來判斷。
  3. 使用線段樹樹狀數組處理這個數組,從而在O(logN)內求出兩個日期之間有多少個工作日。
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

/**
 * 工作日計算工具類<br/>
 * 目前僅支持2017,2018年
 * 
 * @author Corvey
 * @Date 2018年11月9日16:53:52
 */
public class WorkdayUtils {

	/** 預設工作日數據的開始年份 */
	private static final int START_YEAR = 2017;

	/** 預設工作日數據的結束年份 */
	private static final int END_YEAR = 2018;

	/** 起始日期處理策略 */
	private static final BoundaryDateHandlingStrategy START_DATE_HANDLING_STRATEGY = date -> {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		return calendar.get(Calendar.HOUR_OF_DAY) < 12; // 如果開始時間在中午12點前,則當天也算作一天,否則不算
	};

	/** 結束日期處理策略 */
	private static final BoundaryDateHandlingStrategy END_DATE_HANDLING_STRATEGY = date -> {
		return true;	// 結束時間無論幾點,都算作1天
	};

	/** 工作日map,true為補休,false為放假 */
	private static final Map<Integer, Boolean> WORKDAY_MAP = new HashMap<>();

	private static final SegmentTree SEGMENT_TREE;

	static {
		initWorkday(); // 初始化工作日map

		// 計算從START_YEAR到END_YEAR一共有多少天
		int totalDays = 0;
		for (int year = START_YEAR; year <= END_YEAR; ++year) {
			totalDays += getDaysOfYear(year);
		}
		int[] workdayArray = new int[totalDays];	// 將工作日的數據存入到數組
		Calendar calendar = new GregorianCalendar(START_YEAR, 0, 1);
		for (int i = 0; i < totalDays; ++i) {
			// 將日期轉為yyyyMMdd格式的int
			int datestamp = calendar.get(Calendar.YEAR) * 10000 + (calendar.get(Calendar.MONTH) + 1) * 100 + calendar.get(Calendar.DAY_OF_MONTH);
			Boolean isWorkDay = WORKDAY_MAP.get(datestamp);
			if (isWorkDay != null) { // 如果在工作日map里有記錄,則按此判斷工作日
				workdayArray[i] = isWorkDay ? 1 : 0;
			} else { // 如果在工作日map里沒記錄,則按是否為周末判斷工作日
				int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
				workdayArray[i] = (dayOfWeek != Calendar.SATURDAY && dayOfWeek != Calendar.SUNDAY) ? 1 : 0;
			}
			calendar.add(Calendar.DAY_OF_YEAR, 1);
		}
		SEGMENT_TREE = new SegmentTree(workdayArray);	// 生成線段樹
	}

	/**
	 * 計算兩個日期之間有多少個工作日<br/>
	 * @param startDate
	 * @param endDate
	 * @return
	 */
	public static int howManyWorkday(Date startDate, Date endDate) {
		if (startDate.after(endDate)) {
			return howManyWorkday(endDate, startDate);
		}

		Calendar startCalendar = Calendar.getInstance();
		startCalendar.setTime(startDate);
		int startDays = getDaysAfterStartYear(startCalendar) - 1;	// 第一天從0開始
		
		Calendar endCalendar = Calendar.getInstance();
		endCalendar.setTime(endDate);
		int endDays = getDaysAfterStartYear(endCalendar) - 1;	// 第一天從0開始
		
		if (startDays == endDays) {	// 如果開始日期和結束日期在同一天的話
			return isWorkDay(startDate) ? 1 : 0;	// 當天為工作日則返回1天,否則0天
		}
		
		if (!START_DATE_HANDLING_STRATEGY.ifCountAsOneDay(startDate)) { // 根據處理策略,如果開始日期不算一天的話
			++startDays;	// 起始日期向后移一天
		}

		if (!END_DATE_HANDLING_STRATEGY.ifCountAsOneDay(endDate)) { // 根據處理策略,如果結束日期不算一天的話
			--endDays;	// 結束日期向前移一天
		}
		return SEGMENT_TREE.query(startDays, endDays);
	}

	/**
	 * 是否為工作日
	 * @param date
	 * @return
	 */
	public static boolean isWorkDay(Date date) {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		int days = getDaysAfterStartYear(calendar) - 1;
		return SEGMENT_TREE.query(days, days) == 1;
	}

	/**
	 * 	計算從開始年份到這個日期有多少天
	 * @param calendar
	 * @return
	 */
	private static int getDaysAfterStartYear(Calendar calendar) {
		int year = calendar.get(Calendar.YEAR);
		if (year < START_YEAR || year > END_YEAR) {
			throw new IllegalArgumentException(String.format("系統目前僅支持計算%d年至%d年之間的工作日,無法計算%d年!", START_YEAR, END_YEAR, year));
		}
		int days = 0;
		for (int i=START_YEAR; i<year; ++i) {
			days += getDaysOfYear(i);
		}
		days += calendar.get(Calendar.DAY_OF_YEAR);
		return days;
	}
	
	/**
	 * 計算該年有幾天,閏年返回366,平年返回365
	 * @param year
	 * @return
	 */
	private static int getDaysOfYear(int year) {
		return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 ? 366 : 365;
	}

	/**
	 * 初始化工作日Map<br/>
	 * 日期格式必須為yyyyMMdd,true為補休,false為放假,如果本來就是周末的節假日則不需再設置
	 */
	private static void initWorkday() {
		// ---------------2017------------------
		WORKDAY_MAP.put(20170102, false);
		WORKDAY_MAP.put(20170122, true);
		WORKDAY_MAP.put(20170127, false);
		WORKDAY_MAP.put(20170130, false);
		WORKDAY_MAP.put(20170131, false);
		WORKDAY_MAP.put(20170201, false);
		WORKDAY_MAP.put(20170202, false);
		WORKDAY_MAP.put(20170204, true);
		WORKDAY_MAP.put(20170401, true);
		WORKDAY_MAP.put(20170403, false);
		WORKDAY_MAP.put(20170404, false);
		WORKDAY_MAP.put(20170501, false);
		WORKDAY_MAP.put(20170527, true);
		WORKDAY_MAP.put(20170529, false);
		WORKDAY_MAP.put(20170530, false);
		WORKDAY_MAP.put(20170930, true);
		WORKDAY_MAP.put(20171002, false);
		// ------------------2018----------------
		WORKDAY_MAP.put(20180101, false);
		WORKDAY_MAP.put(20180211, true);
		WORKDAY_MAP.put(20180215, false);
		WORKDAY_MAP.put(20180216, false);
		WORKDAY_MAP.put(20180219, false);
		WORKDAY_MAP.put(20180220, false);
		WORKDAY_MAP.put(20180221, false);
		WORKDAY_MAP.put(20180224, true);
		WORKDAY_MAP.put(20180405, false);
		WORKDAY_MAP.put(20180406, false);
		WORKDAY_MAP.put(20180408, true);
		WORKDAY_MAP.put(20180428, true);
		WORKDAY_MAP.put(20180430, false);
		WORKDAY_MAP.put(20180501, false);
		WORKDAY_MAP.put(20180618, false);
		WORKDAY_MAP.put(20180924, false);
		WORKDAY_MAP.put(20180929, true);
		WORKDAY_MAP.put(20180930, true);
		WORKDAY_MAP.put(20181001, false);
		WORKDAY_MAP.put(20181002, false);
		WORKDAY_MAP.put(20181003, false);
		WORKDAY_MAP.put(20181004, false);
		WORKDAY_MAP.put(20181005, false);
	}

	/**
	 * 邊界日期處理策略<br/>
	 * 在計算兩個日期之間有多少個工作日時,有的特殊需求是如果開始/結束的日期在某個時間之前/后(如中午十二點前),則不把當天算作一天<br/>
	 * 因此特將此邏輯分離出來,各自按照不同需求實現該接口即可
	 * @author Corvey
	 * @Date 2018年11月12日15:38:16
	 */
	private interface BoundaryDateHandlingStrategy {
		/** 是否把這個日期算作一天 */
		boolean ifCountAsOneDay(Date date);
	}

	/**
	 * zkw線段樹
	 * @author Corvey
	 */
	private static class SegmentTree {

		private int[] data; // 線段樹數據
		private int numOfLeaf; // 葉子結點個數

		public SegmentTree(int[] srcData) {
			for (numOfLeaf = 1; numOfLeaf < srcData.length; numOfLeaf <<= 1);
			data = new int[numOfLeaf << 1];
			for (int i = 0; i < srcData.length; ++i) {
				data[i + numOfLeaf] = srcData[i];
			}
			for (int i = numOfLeaf - 1; i > 0; --i) {
				data[i] = data[i << 1] + data[i << 1 | 1];
			}
		}

		/** [left, right]區間求和,left從0開始 */
		public int query(int left, int right) {
			if (left > right) {
				return query(right, left);
			}
			left = left + numOfLeaf - 1;
			right = right + numOfLeaf + 1;
			int sum = 0;
			for (; (left ^ right ^ 1) != 0; left >>= 1, right >>= 1) {
				if ((~left & 1) == 1)	sum += data[left ^ 1];
				if ((right & 1) == 1)	sum += data[right ^ 1];
			}
			return sum;
		}
	}

	public static void main(String[] args) throws ParseException {
		System.out.println("測試開始:-------------------");
		DateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH");
		Scanner cin = new Scanner(System.in);
		while (cin.hasNext()) {
			String l = cin.next();
			Date start = df.parse(l);
			String r = cin.next();
			Date end = df.parse(r);
			System.out.println(String.format("%s 到 %s, 有%d個工作日!", df.format(start), df.format(end), howManyWorkday(start, end)));
		}
		cin.close();
	}
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM