如果沒有節日放假調休的話,工作日很好計算,週一到週五就是工作日,但因為有節日放假調休,使得這個計算需要外部放假安排資料來支援。計算原理: 先按照放假安排資料計算,再按照週一週五計算。

下面以LocalDateTime 為例。

1.第一版,沒有使用快取

    /**
* 判斷是否中國工作日,包含法定節假日調整日期,節假日資料holidayData,如果節假日資料不支援年份,將使用週一到週五為工作日來判斷。
* @param localDateTime LocalDateTime
* @param holidayData 放假資訊0表示放假,1表示工作日,如:2021-01-01:0,2021-02-07:1
* @return boolean
*/
public static boolean isChineseWorkDay(LocalDateTime localDateTime, String holidayData){
Objects.requireNonNull(holidayData, "holidayData");
Map<String, Integer> dateTypeMap = StringUtil.convertHolidayDataToMap(holidayData);
Integer dateType = dateTypeMap.get(DateTimeFormatterUtil.formatToDateStr(localDateTime));
if(dateType != null){
return dateType == 1 ? true : false;
}
return isWorkDay(localDateTime);
} // StringUtil.convertHolidayDataToMap /**
* 轉換節日資料為map
* @param holidayData 節日map
* @return 返回節日map
*/
public static Map<String, Integer> convertHolidayDataToMap(String holidayData){
Map<String, Integer> dateTypeMap = new HashMap<>();
if(isEmpty(holidayData)){
return dateTypeMap;
} String[] dateTypeArr = holidayData.replace(" ", "").split(",");
for(String dateType : dateTypeArr){
String[] arr = dateType.split(":");
dateTypeMap.put(arr[0], Integer.valueOf(arr[1]));
}
return dateTypeMap;
} /**
* 判斷是否工作日 (週一到週五)
* @param localDateTime LocalDateTime
* @return boolean
*/
public static boolean isWorkDay(LocalDateTime localDateTime){
int dayOfWeek = getDayOfWeek(localDateTime);
if(dayOfWeek == 6 || dayOfWeek == 7){
return false;
}else{
return true;
}
}

這個方法,先將放假安排資料解析成Map,然後對比,最後使用週一到週五判斷。

2.第二版,使用快取優化

第一版中,每次呼叫都先將放假安排資料解析成Map,但其實是不需要的,因為放假安排資料每年只發布一次(特殊情況除外),一年都不需要變化,這些資料第一次呼叫時放進快取,後面直接使用,有變化時再更新快取。

快取使用本地快取和Redis快取都可以,本地快取速度更快一些,下面使用本地快取。

    public static boolean isChineseWorkDay2(LocalDateTime localDateTime, String holidayData){
Objects.requireNonNull(holidayData, "holidayData");
Map<String, Integer> dateTypeMap = StringUtil.convertHolidayDataToMapUseCache(holidayData);
Integer dateType = dateTypeMap.get(DateTimeFormatterUtil.formatToDateStr(localDateTime));
if(dateType != null){
return dateType == 1 ? true : false;
}
return isWorkDay(localDateTime);
} //StringUtil.convertHolidayDataToMapUseCache /**
* 轉換節日資料為map,使用快取提高效能
* @param holidayData 節日map
* @return 返回節日map
*/
@SuppressWarnings("unchecked")
public static Map<String, Integer> convertHolidayDataToMapUseCache(String holidayData){
Map<String, Integer> dateTypeMap = new HashMap<>();
//引數為空,直接返回
if(isEmpty(holidayData)){
return dateTypeMap;
} //查詢快取
dateTypeMap = (Map<String, Integer>)CommonCache.get(holidayData); //快取存在,返回快取
if(CollectionUtil.isNotEmpty(dateTypeMap)){
return dateTypeMap;
} //快取不存在,先設定快取然後返回
Supplier<Object> supplier = new Supplier<Object>() {
@Override
public Object get() {
Map<String, Integer> dateTypeMap = new HashMap<>();
String[] dateTypeArr = holidayData.replace(" ", "").split(",");
for(String dateType : dateTypeArr){
String[] arr = dateType.split(":");
dateTypeMap.put(arr[0], Integer.valueOf(arr[1]));
}
return dateTypeMap;
}
};
return (Map<String, Integer>)CommonCache.get(holidayData, supplier);
} /**
* 判斷是否工作日 (週一到週五)
* @param localDateTime LocalDateTime
* @return boolean
*/
public static boolean isWorkDay(LocalDateTime localDateTime){
int dayOfWeek = getDayOfWeek(localDateTime);
if(dayOfWeek == 6 || dayOfWeek == 7){
return false;
}else{
return true;
}
}

快取使用了WeakHashMap實現快取自動清理,使用ReentrantReadWriteLock實現讀寫執行緒安全。詳細程式碼見com.xkzhangsan.time.utils.CommonCache,核心程式碼片段如下:

    /**
* 從快取池中查詢值
*
* @param key
* 鍵
* @return 值
*/
public V get(K key) {
lock.readLock().lock();
try {
return cache.get(key);
} finally {
lock.readLock().unlock();
}
} /**
* 從快取池中查詢值,沒有時嘗試生成
*
* @param key
* 鍵
* @param supplier 提供者
* @return 值
*/
public V get(K key, Supplier<V> supplier) {
V value = get(key);
if (value == null && supplier != null) {
lock.writeLock().lock();
try {
value = cache.get(key);
// 雙重檢查,防止在競爭鎖的過程中已經有其它執行緒寫入
if (null == value) {
try {
value = supplier.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
cache.put(key, value);
}
} finally {
lock.writeLock().unlock();
}
}
return value;
}


3. 二種實現效能對比

這裡以2021年放假資訊為例,分別呼叫100萬次。忽略第一次建立快取的時間,從第二次開始,測試資料如下:

 2021-01-01:0,2021-02-07:1,2021-02-11:0,2021-02-12:0,2021-02-15:0,2021-02-16:0,2021-02-17:0,2021-02-20:1,2021-04-05:0,2021-04-25:1,2021-05-03:0,2021-05-04:0,2021-05-05:0,2021-05-08:1,2021-06-14:0,2021-09-18:1,2021-09-20:0,2021-09-21:0,2021-09-26:1,2021-10-01:0,2021-10-04:0,2021-10-05:0,2021-10-06:0,2021-10-07:0,2021-10-09:1

    @Test
public void isChineseWorkDay1(){
//2021年放假資訊
String holidayData = "2021-01-01:0,2021-02-07:1,2021-02-11:0,2021-02-12:0,2021-02-15:0,2021-02-16:0,2021-02-17:0,2021-02-20:1,2021-04-05:0,2021-04-25:1,2021-05-03:0,2021-05-04:0,2021-05-05:0,2021-05-08:1,2021-06-14:0,2021-09-18:1,2021-09-20:0,2021-09-21:0,2021-09-26:1,2021-10-01:0,2021-10-04:0,2021-10-05:0,2021-10-06:0,2021-10-07:0,2021-10-09:1";
//指定日期是否是工作日
long s = 0;
for (int i = 0; i < 1000001; i++) {
if(i==1){
s = System.currentTimeMillis();
}
DateTimeCalculatorUtil.isChineseWorkDay(LocalDateTime.now(), holidayData);
}
System.out.println("isChineseWorkDay1 cost1:"+(System.currentTimeMillis()-s));
} @Test
public void isChineseWorkDay2(){
//2021年放假資訊
String holidayData = "2021-01-01:0,2021-02-07:1,2021-02-11:0,2021-02-12:0,2021-02-15:0,2021-02-16:0,2021-02-17:0,2021-02-20:1,2021-04-05:0,2021-04-25:1,2021-05-03:0,2021-05-04:0,2021-05-05:0,2021-05-08:1,2021-06-14:0,2021-09-18:1,2021-09-20:0,2021-09-21:0,2021-09-26:1,2021-10-01:0,2021-10-04:0,2021-10-05:0,2021-10-06:0,2021-10-07:0,2021-10-09:1";
//指定日期是否是工作日
long s = 0;
for (int i = 0; i < 1000001; i++) {
if(i==1){
s = System.currentTimeMillis();
}
DateTimeCalculatorUtil.isChineseWorkDay2(LocalDateTime.now(), holidayData);
}
System.out.println("isChineseWorkDay2 cost2:"+(System.currentTimeMillis()-s));
}

結果(單位:ms):

isChineseWorkDay1 cost1:5589
isChineseWorkDay2 cost2:366

可以看到,使用快取後效能對比 5589/366=15.27 , 速度提高15倍多,程式碼效能的小優化,大量呼叫後會被累加放大,優化非常值得!

原始碼地址:https://github.com/xkzhangsan/xk-time