如果沒有節日放假調休的話,工作日很好計算,周一到周五就是工作日,但因為有節日放假調休,使得這個計算需要外部放假安排數據來支持。計算原理: 先按照放假安排數據計算,再按照周一周五計算。
下面以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