背景:项目中涉及到需要计算两个时间节点的时间差,一开始设想的是精确到小时数,考虑上下班和午休,以及节假日。
这位道友的工具类排除了周末,离我的需求很接近了,只需要再排除节假日。说到节假日,就得想到调休补班日,假期的时间应该减掉,那么补班日的时间就应该加回来。
初版:
package com.xxxx.xxx.xxxx.utils;
import lombok.Getter;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* 工作时间计算
* @author tkai
* @description: 非常 niubility 的处理时间计算,排除了节假日(含调休),以及午休时间。
* @date 2023/8/10 10:04
*/
public class WorkTimeCalculate {
/**
* 上班时间
*/
private static final LocalTime WORKING_START_TIME = LocalTime.of(9, 0);
/**
* 下班时间
*/
private static final LocalTime WORKING_END_TIME = LocalTime.of(17, 0);
/**
* 午休开始时间
*/
private static final LocalTime NOON_BREAK_START_TIME = LocalTime.of(12, 0);
/**
* 午休结束时间
*/
private static final LocalTime NOON_BREAK_END_TIME = LocalTime.of(13, 0);
/**
* 调休日:非周末但是休息的日期列表
*/
@Getter
private static final List<LocalDate> holidays = new ArrayList<>();
/**
* 补班日:周末但是上班的日期
*/
@Getter
private static final List<LocalDate> workdays = new ArrayList();
//初始化调休日和补班日 后面可考虑从数据库查
static {
//调休日
holidays.add(LocalDate.of(2022,1,3));
holidays.add(LocalDate.of(2022,1,31));
holidays.add(LocalDate.of(2022,2,1));
holidays.add(LocalDate.of(2022,2,2));
holidays.add(LocalDate.of(2022,2,3));
holidays.add(LocalDate.of(2022,2,4));
holidays.add(LocalDate.of(2022,4,4));
holidays.add(LocalDate.of(2022,4,5));
holidays.add(LocalDate.of(2022,5,2));
holidays.add(LocalDate.of(2022,5,3));
holidays.add(LocalDate.of(2022,5,4));
holidays.add(LocalDate.of(2022,6,3));
holidays.add(LocalDate.of(2022,9,12));
holidays.add(LocalDate.of(2022,10,3));
holidays.add(LocalDate.of(2022,10,4));
holidays.add(LocalDate.of(2022,10,5));
holidays.add(LocalDate.of(2022,10,6));
holidays.add(LocalDate.of(2022,10,7));
holidays.add(LocalDate.of(2023,1,2));
holidays.add(LocalDate.of(2023,1,23));
holidays.add(LocalDate.of(2023,1,24));
holidays.add(LocalDate.of(2023,1,25));
holidays.add(LocalDate.of(2023,1,26));
holidays.add(LocalDate.of(2023,1,27));
holidays.add(LocalDate.of(2023,4,5));
holidays.add(LocalDate.of(2023,5,1));
holidays.add(LocalDate.of(2023,5,2));
holidays.add(LocalDate.of(2023,5,3));
holidays.add(LocalDate.of(2023,6,22));
holidays.add(LocalDate.of(2023,6,23));
holidays.add(LocalDate.of(2023,9,29));
holidays.add(LocalDate.of(2023,10,2));
holidays.add(LocalDate.of(2023,10,3));
holidays.add(LocalDate.of(2023,10,4));
holidays.add(LocalDate.of(2023,10,5));
holidays.add(LocalDate.of(2023,10,6));
//补班日
workdays.add(LocalDate.of(2022,1,29));
workdays.add(LocalDate.of(2022,1,30));
workdays.add(LocalDate.of(2022,4,2));
workdays.add(LocalDate.of(2022,4,24));
workdays.add(LocalDate.of(2022,5,7));
workdays.add(LocalDate.of(2022,10,8));
workdays.add(LocalDate.of(2022,10,9));
workdays.add(LocalDate.of(2023,1,28));
workdays.add(LocalDate.of(2023,1,29));
workdays.add(LocalDate.of(2023,4,23));
workdays.add(LocalDate.of(2023,5,6));
workdays.add(LocalDate.of(2023,6,25));
workdays.add(LocalDate.of(2023,10,7));
workdays.add(LocalDate.of(2023,10,8));
}
public static Integer getWorkTime(LocalDateTime startDateTime, LocalDateTime endDateTime) {
if (startDateTime.compareTo(endDateTime) > 0) {
throw new RuntimeException("参数错误,开始时间大于结束时间。");
}
int diff = 0;
while (true) {
//休息日 开始时间后移至第二天的开始工作时间
while (needPass(startDateTime)) {
startDateTime = LocalDateTime.of(startDateTime.toLocalDate(), WORKING_START_TIME).plusDays(1);
}
//休息日 结束时间后移至第二天的开始工作时间
while (needPass(endDateTime)) {
endDateTime = LocalDateTime.of(endDateTime.toLocalDate(), WORKING_START_TIME).plusDays(1);
}
// 跨天处理
if (startDateTime.getDayOfYear() == endDateTime.getDayOfYear()) {
int diffSecond = getDiffSecond(startDateTime, endDateTime);
diff = diffSecond + diff;
break;
}
int diffSecond = getDiffSecond(startDateTime, LocalDateTime.of(startDateTime.toLocalDate(), WORKING_END_TIME));
diff = diffSecond + diff;
startDateTime = LocalDateTime.of(startDateTime.toLocalDate(), WORKING_START_TIME).plusDays(1);
}
System.out.println("DIFF(hours): " + Double.valueOf(diff) / 3600 +"小时");
return diff;
}
private static int getDiffSecond(LocalDateTime startDateTime, LocalDateTime endDateTime) {
LocalTime startTime = startDateTime.toLocalTime();
LocalTime endTime = endDateTime.toLocalTime();
// diff单位:秒
int diff = 0;
// 开始时间切移
if (startTime.isBefore(WORKING_START_TIME)) {
startTime = WORKING_START_TIME;
} else if (startTime.isAfter(NOON_BREAK_START_TIME) && startTime.isBefore(NOON_BREAK_END_TIME)) {
startTime = NOON_BREAK_START_TIME;
} else if (startTime.isAfter(WORKING_END_TIME)) {
startTime = WORKING_END_TIME;
}
// 结束时间切移
if (endTime.isBefore(WORKING_START_TIME)) {
endTime = WORKING_START_TIME;
} else if (endTime.isAfter(NOON_BREAK_START_TIME) && endTime.isBefore(NOON_BREAK_END_TIME)) {
endTime = NOON_BREAK_START_TIME;
} else if (endTime.isAfter(WORKING_END_TIME)) {
endTime = WORKING_END_TIME;
}
// 午休时间判断处理
if (startTime.compareTo(NOON_BREAK_START_TIME) <= 0 && endTime.compareTo(NOON_BREAK_END_TIME) >= 0) {
diff = diff + 60 * 60;
}
diff = endTime.toSecondOfDay() - startTime.toSecondOfDay() - diff;
return diff;
}
/**
* 计算两个时间之间的天数 每天按24小数计算 不考虑上下班 不足一天的部分保留两位小数
* 特殊情况:开始时间在休息日,则开始时间按下个工作日的00:00:00开始计算
* 特殊情况:结束时间在休息日,则结束时间按上个工作日的13:59:59开始计算
* @param startDateTime
* @param endDateTime
* @return
*/
public static BigDecimal getWorkDays(LocalDateTime startDateTime, LocalDateTime endDateTime) {
if (startDateTime.compareTo(endDateTime) > 0) {
throw new RuntimeException("参数错误,开始时间大于结束时间。");
}
int fullDays = 0;
long minute = 0;
//特殊情况:开始时间在休息日,则开始时间按下个工作日的00:00:00开始计算
if(needPass(startDateTime)){
startDateTime = startDateTime.toLocalDate().atStartOfDay().plusDays(1);
}
//特殊情况:结束时间在休息日,则结束时间按上个工作日的13:59:59开始计算
if(needPass(endDateTime)){
endDateTime = endDateTime.toLocalDate().atStartOfDay();
}
while (startDateTime.compareTo(endDateTime) < 0) {
while (needPass(startDateTime)) {
startDateTime = startDateTime.plusDays(1);
}
while (needPass(endDateTime)) {
endDateTime = endDateTime.plusDays(1);
}
if (startDateTime.getDayOfYear() == endDateTime.getDayOfYear()) {
// 按分钟计算
minute = Duration.between(startDateTime, endDateTime).toMinutes();
break;
}
startDateTime = startDateTime.plusDays(1);
fullDays += 1;
}
//折算不足一天的时间
BigDecimal oddDay = new BigDecimal(minute).divide(new BigDecimal(24*60),2, RoundingMode.HALF_UP);
return new BigDecimal(fullDays).add(oddDay);
}
/**
* 当天休息还是工作
* @param time
* @return
*/
private static boolean needPass(LocalDateTime time){
if(time.getDayOfWeek() == DayOfWeek.SATURDAY||time.getDayOfWeek() == DayOfWeek.SUNDAY){
if(isWorkday(time)){
return false;
}
return true;
}else{
if(isHoliday(time)){
return true;
}
return false;
}
}
/**
* 是否调休日
* @param time
* @return
*/
private static boolean isHoliday(LocalDateTime time){
LocalDate date = time.toLocalDate();
if(holidays.contains(date)){
return true;
}
return false;
}
/**
* 是否补班日
* @param time
* @return
*/
private static boolean isWorkday(LocalDateTime time){
LocalDate date = time.toLocalDate();
if(workdays.contains(date)){
return true;
}
return false;
}
public static void main(String[] args) {
WorkTimeCalculate.getWorkTime(LocalDateTime.of(2022, 4, 29, 11, 30, 0),
LocalDateTime.of(2022, 5, 17, 20, 30, 0));
System.out.println(WorkTimeCalculate.getWorkDays(LocalDateTime.of(2022, 4, 29, 12, 30, 0),
LocalDateTime.of(2022, 5, 7, 13, 0, 0)));
}
}
然而,跟需求方讨论后得知,不需要精确到小时数,同时,时间也不考虑上下班,每天以24小时计算,计算的单位为天,保留2位小时。
好像是简单了很多,于是基于上面的版本,做了一些修改。
大体思路:遍历开始时间到结束时间之间的每一天,判断当天是不是上班,不是就后移至第二天的0点,否则开始时间后移一天,整天数加1,直到开始时间和结束时间为同一天。随后计算相差的小时数,除以24得到零散部分,这部分可能是正数也可能是负数。最后整天数与零散部分相加,就得到保留二位小数的时间差天数啦。
考虑到调休日和补班日每年都会新增,这一部分从数据库读取比较合理。最后做成工具类如下:
package com.xxxx.xxxxx.xxxx.utils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* @author tkai
* @description: 工作时长计算
* @date 2023/8/11 15:25
*/
public class WorkTimeUtil {
/**
* 计算两个时间之间的天数 每天按24小数计算 不考虑上下班 不足一天的部分保留两位小数
* 特殊情况:开始时间在休息日,则开始时间按下个工作日的00:00:00开始计算
* 特殊情况:结束时间在休息日,则结束时间按上个工作日的13:59:59开始计算
* @param startDateTime
* @param endDateTime
* @return
*/
public static BigDecimal getWorkDays(LocalDateTime startDateTime, LocalDateTime endDateTime,List<LocalDate> holidays,List<LocalDate> workdays) {
if (startDateTime.compareTo(endDateTime) > 0) {
// throw new RuntimeException("参数错误,开始时间大于结束时间。");
return new BigDecimal(0.00);
}
int fullDays = 0;
long minute = 0;
//特殊情况:开始时间在休息日,则开始时间按下个工作日的00:00:00开始计算
if(needPass(startDateTime,holidays,workdays)){
startDateTime = startDateTime.toLocalDate().atStartOfDay().plusDays(1);
}
//特殊情况:结束时间在休息日,则结束时间按上个工作日的23:59:59开始计算
if(needPass(endDateTime,holidays,workdays)){
endDateTime = endDateTime.toLocalDate().atStartOfDay();
}
while (startDateTime.compareTo(endDateTime) < 0) {
while (needPass(startDateTime,holidays,workdays)) {
startDateTime = startDateTime.plusDays(1);
}
while (needPass(endDateTime,holidays,workdays)) {
endDateTime = endDateTime.plusDays(1);
}
if (startDateTime.getDayOfYear() == endDateTime.getDayOfYear()) {
// 按分钟计算
minute = Duration.between(startDateTime, endDateTime).toMinutes();
break;
}
startDateTime = startDateTime.plusDays(1);
fullDays += 1;
}
//折算不足一天的时间
BigDecimal oddDay = new BigDecimal(minute).divide(new BigDecimal(24*60),2, RoundingMode.HALF_UP);
return new BigDecimal(fullDays).add(oddDay);
}
/**
* 当天休息还是工作
* @param time
* @return
*/
private static boolean needPass(LocalDateTime time,List<LocalDate> holidays,List<LocalDate> workdays){
if(time.getDayOfWeek() == DayOfWeek.SATURDAY||time.getDayOfWeek() == DayOfWeek.SUNDAY){
if(isWorkday(time,workdays)){
return false;
}
return true;
}else{
if(isHoliday(time,holidays)){
return true;
}
return false;
}
}
/**
* 是否调休日
* @param time
* @return
*/
private static boolean isHoliday(LocalDateTime time,List<LocalDate> holidays){
LocalDate date = time.toLocalDate();
if(holidays.contains(date)){
return true;
}
return false;
}
/**
* 是否补班日
* @param time
* @return
*/
private static boolean isWorkday(LocalDateTime time, List<LocalDate> workdays){
LocalDate date = time.toLocalDate();
if(workdays.contains(date)){
return true;
}
return false;
}
}