给定一个时间,根据配置的节假日和休息时间,跳过节假日与休息时间,计算推进指定时间之后的时间。转载请附带原文链接

解释一下思路:首先锚定开始时间,由于给定时间可能在节假日或者休息时间(当然也可以在调用方直接拦截,这里没有拦截),所以需要判断并重新计算开始时间,这里的请参看下面代码注释。锚定开始时间后,思路就比较清晰了,总时间(我这里用的是小时单位,如果需要变成分钟,需要自己转)转分钟,除以每日工作时长得到需跨越天数,取余得剩余时长,最后添加即可

下面给出工具类源码:

package cn.li.project.util;

import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.annotation.JSONField;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import org.springframework.data.annotation.Id;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Date;
import java.util.List;

/**
 * @author li
 */
public class DateUtil {

    //休息时间
    private static final String bidRestTime = "18:00-8:00";

    /**
     * 计算竞价截止时间,自动跳过节假日,此方法会抹除秒级时间
     * ####核心代码####
     * @param date 当前时间
     * @param time 报价时效
     * @return 竞价截止时间
     */
    public Date computeBidBlockingTimeWithoutHoliday(Date date, Integer time) {
        Date targetDate;

        LocalDateTime beginDate = LocalDateTimeUtil.of(date);

        List<CalendarInfo> calendarDays = getNextMonthsCalendarDays(beginDate.toLocalDate(), 2L);

        boolean notBeginningDay = false;
        int originHour = beginDate.getHour();
        int originMinute = beginDate.getMinute();
        //如果当前日期所处时间是节假日,则将开始日期推进到下一个工作日,并设置开始时间为00:00:00,以便没有配置休息时间时使用
        if (isHoliday(beginDate)) {
            LocalDateTime nextWorkDay = getNextWorkDay(beginDate);
            notBeginningDay = true;
            beginDate = LocalDateTime.of(
                    nextWorkDay.getYear(),
                    nextWorkDay.getMonthValue(),
                    nextWorkDay.getDayOfMonth(),
                    0, 0, 0);
        }

        if (!StrUtil.isBlank(bidRestTime)) {
            //如果有休息时间配置
            String[] split = bidRestTime.split("-");
            String workingStart = split[1];
            String workingEnd = split[0];

            String[] workingStartSplit = workingStart.split(":");
            String[] workingEndSplit = workingEnd.split(":");
            //分割时间,得到工作开始、结束时间的小时和分钟位
            int workingStartHour = Integer.parseInt(workingStartSplit[0]);
            int workingStartMinute = Integer.parseInt(workingStartSplit[1]);
            int workingEndHour = Integer.parseInt(workingEndSplit[0]);
            int workingEndMinute = Integer.parseInt(workingEndSplit[1]);

            //不考虑夜班,即工作跨日
            if (workingEndHour < 0 || workingEndHour > 23 || workingStartHour > workingEndHour ||
                    (workingStartHour == workingEndHour && workingStartMinute < workingEndMinute) ||
                    workingStartMinute < 0 || workingStartMinute > 59 ||
                    workingEndMinute < 0 || workingEndMinute > 59) {
                throw new RuntimeException("休息时间配置不正确");
            }

            //判断上下班时间,规整日期开始计算的时间,正常工作日上班时间内不处理
            if (notBeginningDay || originHour < workingStartHour ||
                    (originHour == workingStartHour && originMinute < workingStartMinute)) {
                //如果是在节假日或者当前时间在上班开始时间之前,需要把开始时间调整到当天的上班时间
                beginDate = beginDate.withHour(workingStartHour).withMinute(workingStartMinute).withSecond(0);
            } else if ((originHour == workingEndHour && originMinute > workingEndMinute) ||
                    originHour > workingEndHour) {
                //如果当前时间在下班时间之后,把时间推进到第二天的上班时间
                beginDate = getNextWorkDayAndResetWorkTime(beginDate, calendarDays, workingStartHour, workingStartMinute);
            }

            //设置一个固定日期,以便计算每天的工作时间
            String fixDate = "2000-01-01 ";
            String formatStr = "yyyy-MM-dd HH:mm";
            int workMinutes = (int) cn.hutool.core.date.DateUtil.between(
                    cn.hutool.core.date.DateUtil.parse(fixDate + workingStart, formatStr),
                    cn.hutool.core.date.DateUtil.parse(fixDate + workingEnd, formatStr),
                    DateUnit.MINUTE
            );

            //将计算时长转换为分钟,计算总共需要跨几日以及跨日期后剩余的分钟数
            int minutesTime = time * 60;
            int toPlusDay = minutesTime / workMinutes;
            int restMinutes = minutesTime % workMinutes;

            //推进天数
            for (int i = 0; i < toPlusDay; i++) {
                beginDate = getNextWorkDay(beginDate, calendarDays);
            }

            //保存推进后的日期
            LocalDateTime workingEndTime = LocalDateTime.of(
                    beginDate.getYear(), beginDate.getMonthValue(),
                    beginDate.getDayOfMonth(), workingEndHour, workingEndMinute);

            //推进指定的分钟数
            LocalDateTime afterDateTime = beginDate.plusMinutes(restMinutes);
            if (afterDateTime.isAfter(workingEndTime)) {
                //若推进后时间在当天下班时间之后,则计算超出下班时间后的时间,并添加到第二天上班时间之后
                int beyondMinutes = (int) LocalDateTimeUtil.between(workingEndTime, afterDateTime, ChronoUnit.MINUTES);
                beginDate = getNextWorkDayAndResetWorkTime(beginDate, calendarDays, workingStartHour, workingStartMinute)
                        .plusMinutes(beyondMinutes);
            } else {
                beginDate = afterDateTime;
            }
        } else {
            //没有休息时间配置
            int day = time / 24;
            int hour = time % 24;

            //推进天数
            for (int i = 0; i < day; i++) {
                beginDate = getNextWorkDay(beginDate, calendarDays);
            }

            int currentHour = beginDate.getHour();
            int totalHours = currentHour + hour;
            //若推进后
            if (totalHours > 23) {
                beginDate = getNextWorkDay(beginDate, calendarDays).withHour(totalHours - 23);
            } else {
                beginDate = beginDate.plusHours(hour);
            }
        }
        String format = "yyyy-MM-dd HH:ss:mm";
        targetDate = cn.hutool.core.date.DateUtil.parse(beginDate.format(DateTimeFormatter.ofPattern(format)), format);

        return targetDate;
    }

    /**
     * 获取接下来指定月数的内假期配置,代码省略
     */
    private List<CalendarInfo> getNextMonthsCalendarDays(LocalDate toLocalDate, Long month) {
        return Collections.emptyList();
    }

    /**
     * 从给定节假日配置中向后推进一个工作日,从缓存的节假日列表中推进
     * @param beginDate 开始日期
     * @param calendarInfoList 给定节假日配置
     * @return 计算后的日期
     */
    private LocalDateTime getNextWorkDay(LocalDateTime beginDate, List<CalendarInfo> calendarInfoList) {
        beginDate = beginDate.plusDays(1);

        while (isHoliday(beginDate, calendarInfoList)) {
            beginDate = beginDate.plusDays(1);
        }

        return beginDate;
    }

    /**
     * 从给定节假日配置中向后推进一个工作日,并将时间设置为上班时间
     * @param beginDate 开始日期
     * @param calendarInfoList 给定节假日配置
     * @return 计算后的日期
     */
    private LocalDateTime getNextWorkDayAndResetWorkTime(LocalDateTime beginDate,
                                                         List<CalendarInfo> calendarInfoList,
                                                         int workingStartHour,
                                                         int workingStartMinute) {
        beginDate = beginDate.plusDays(1);

        while (isHoliday(beginDate, calendarInfoList)) {
            beginDate = beginDate.plusDays(1);
        }

        beginDate = beginDate.withHour(workingStartHour).withMinute(workingStartMinute).withSecond(0);

        return beginDate;
    }

    /**
     * 从给定节假日配置中向后推进一个工作日,实时查询数据库的节假日列表推进
     * @param beginDate 开始日期
     * @return 计算后的日期
     */
    private LocalDateTime getNextWorkDay(LocalDateTime beginDate) {
        LocalDateTime newDate = beginDate.plusDays(1);

        while (isHoliday(newDate)) {
            newDate = newDate.plusDays(1);
        }

        return newDate;
    }

    /**
     * 查询数据库配置当天是否是节假日,需要根据date的年月日精准匹配
     * 代码省略
     */
    private boolean isHoliday(LocalDateTime date) {

        return true;
    }

    /**
     * 从给定节假日配置缓存中看当天是否是节假日,需要根据date的年月日精准匹配
     * 代码省略
     */
    private boolean isHoliday(LocalDateTime newDate, List<CalendarInfo> calendarInfoList) {

        return true;
    }

}

/**
* 代码中所用实体类,建议配置时,全部日期都默认为工作日,只保存节假日,周六周日调休的不保存,不然需要在isHoliday方法中进行比较细致的判断
*/
@Data
@Builder
@ToString
@TableName("calendar_info")
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
class CalendarInfo {

    private static final long serialVersionUID = -1640722497108507541L;

    /**
     * ID
     */
    @Id
    private BigDecimal id;

    /**
     * 创建人
     */
    private String createdBy;

    /**
     * 创建时间
     */
    @JSONField(format = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @TableField(value = "created_time", fill = FieldFill.INSERT)
    private Date createdTime;

    /**
     * 更新人
     */
    private String updatedBy;

    /**
     * 更新时间
     */
    @JSONField(format = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
    private Date updatedTime;

    /**
     * 是否有效(1:是,0:否)
     */
    @TableField(value = "is_valid", fill = FieldFill.INSERT)
    private String isValid;

    /**
     * 实体类保存日期
     */
    @TableField("calendar_current_date")
    private LocalDate currentDate;

    /**
     * 日期所在年
     */
    @TableField("calendar_year")
    private Integer year;

    /**
     * 日期所在月
     */
    @TableField("calendar_month")
    private Integer month;

    /**
     * 日期所在日
     */
    @TableField("calendar_day")
    private Integer day;

    /**
     * 日期所在星期几:1,2,3,4,5,6,7之一
     */
    @TableField("calendar_week_day")
    private Integer weekDay;

}