Java工具-擷取某月份天數、某月最後一天、某月工作日天數(支援自定義節假日)
因為之前在項目中有一個工作日志的功能,是以在網上找了一些相關的工具類,都是零零散散,我在這總結一下。廢話不多說,上代碼!
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @ClassName WorkDayUtil 計算日期、工作日相關的工具類
* @Auther JunKai
* @Date 2019/12/24
*/
public class WorkDayUtil {
/** 預設工作日資料的開始年份 */
private static final int START_YEAR = 2017;
/** 預設工作日資料的結束年份 */
private static final int END_YEAR = 5020;
/** 工作日map,true為補休,false為放假 */
public static final Map<Integer, Boolean> WORKDAY_MAP = new HashMap<>();
private static final SegmentTree SEGMENT_TREE;
private WorkDayUtil() {
}
/** 擷取一個月天數 */
public static int getDaysOfMonth(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
}
/** 擷取一個月的最後一天 */
public static String getLastDay(Date date) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
int lastDays = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
Date time = calendar.getTime();
time.setDate(lastDays);
return df.format(time);
}
/** 擷取一個月的第一天 */
public static String getFirstDay(Date date) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
//calendar.add(Calendar.MONTH, -1);
calendar.set(Calendar.DAY_OF_MONTH, 1);
String time = df.format(calendar.getTime());
return time;
}
/**
* 計算兩個日期之間有多少個工作日<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);
}
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); // 生成線段樹
}
/**
* 判斷某一日期是否為工作日
* @param date
* @return
*/
public static boolean isWorkDay(int date) {
if(WORKDAY_MAP.get(date)== null) {
Calendar calendar = Calendar.getInstance();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
try {
calendar.setTime(sdf.parse(String.valueOf(date)));
} catch (ParseException e) {
e.printStackTrace();
}
//如果當天不是補休也不是放假,但是為周末,則為非工作日
if(calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY || calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY){
return false;
}
} else {
//判斷如何當天是法定假日,則為非工作日
if(WORKDAY_MAP.get(date) == false) {
return false;
}
//判斷如何當天是補休,則為工作日
if(WORKDAY_MAP.get(date)== true) {
return true;
}
}
return true;
}
/**
* 是否為工作日
* @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() {
// ---------------2020------------------
WORKDAY_MAP.put(20200101, false);
WORKDAY_MAP.put(20200124, false);
WORKDAY_MAP.put(20200125, false);
WORKDAY_MAP.put(20200126, false);
WORKDAY_MAP.put(20200127, false);
WORKDAY_MAP.put(20200128, false);
WORKDAY_MAP.put(20200129, false);
WORKDAY_MAP.put(20200130, false);
WORKDAY_MAP.put(20200201, true);
// ---------------2019------------------
WORKDAY_MAP.put(20190101, false);
WORKDAY_MAP.put(20190202, true);
WORKDAY_MAP.put(20190203, true);
WORKDAY_MAP.put(20190204, false);
WORKDAY_MAP.put(20190205, false);
WORKDAY_MAP.put(20190206, false);
WORKDAY_MAP.put(20190207, false);
WORKDAY_MAP.put(20190208, false);
WORKDAY_MAP.put(20190209, false);
WORKDAY_MAP.put(20190210, false);
WORKDAY_MAP.put(20190405, false);
WORKDAY_MAP.put(20190501, false);
WORKDAY_MAP.put(20190502, false);
WORKDAY_MAP.put(20190503, false);
WORKDAY_MAP.put(20190505, true);
WORKDAY_MAP.put(20190607, false);
WORKDAY_MAP.put(20190913, false);
WORKDAY_MAP.put(20190929, true);
WORKDAY_MAP.put(20191001, false);
WORKDAY_MAP.put(20191002, false);
WORKDAY_MAP.put(20191003, false);
WORKDAY_MAP.put(20191004, false);
WORKDAY_MAP.put(20191007, false);
WORKDAY_MAP.put(20191012, true);
// ------------------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/>
* 是以特将此邏輯分離出來,各自按照不同需求實作該接口即可
* @Auther JunKai
* @Date 2019/12/24
*/
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;
}
}
/** 起始日期處理政策 */
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天
};
}
好了,這工具類就完事了!