Loading

计算时间差(排除休息日)并格式化

package cn.smarthse.common.util.date;

import cn.hutool.core.collection.ConcurrentHashSet;
import cn.smarthse.common.util.passwordLog.HttpUtil;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;

import javax.annotation.PostConstruct;
import java.io.Serializable;
import java.lang.reflect.Type;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * @Author: DengJia
 * @Date: 2023/10/20
 * @Description: 时间工具类
 */

@Slf4j
public class TimeUtil {
    /**
     * 假期列表API
     */
    private static final String HOLIDAY_API = "https://timor.tech/api/holiday/year/";

    /**
     * 年度假期集合
     */
    private static final Map<Integer, Map<Boolean, ConcurrentHashSet<LocalDate>>> YEAR_HOLIDAY_MAP_SET = new HashMap<>();

    /**
     * 初始化获取年度假期列表
     */
    @PostConstruct
    private void init() {
        Set<Integer> yearSet = new HashSet<>();
        yearSet.add(LocalDate.now().getYear());
        startMaintenanceYearHolidayConcurrentHashSet(yearSet);
    }

    public static void main(String[] args) {
        String timeString1 = "2023-09-29 09:01:02.123";
        String timeString2 = "2023-10-08 11:04:06.128";
        log.info("时间差:" + diffFormat(timeString1, timeString2, ChronoUnit.MILLIS));
    }

    /**
     * 计算日期差值(排除休息日)并格式化,默认:秒。
     *
     * @param st 开始时间
     * @param et 结束时间
     * @return 差值格式化
     */
    private static String diffFormat(String st, String et) {
        return diffFormat(st, et, ChronoUnit.SECONDS);
    }

    /**
     * 计算日期差值(排除休息日)并格式化,单位:自定义。
     *
     * @param st   开始时间
     * @param et   结束时间
     * @param unit 时间单位
     * @return 差值格式化
     */
    private static String diffFormat(String st, String et, ChronoUnit unit) {
        long differenceUnits = calculateTimeDifference(st, et, unit);
        return formatTimeDifferenceUnit(differenceUnits, unit);
    }

    /**
     * 计算差值,默认:秒。
     *
     * @param ss 开始时间
     * @param es 结束时间
     * @return 差值
     */
    private static Long calculateTimeDifference(String ss, String es) {
        // 默认单位:秒
        return calculateTimeDifference(ss, es, ChronoUnit.SECONDS);
    }

    /**
     * 计算差值,单位:自定义。
     *
     * @param ss   开始时间
     * @param es   结束时间
     * @param unit 时间单位
     * @return 差值
     */
    private static long calculateTimeDifference(String ss, String es, ChronoUnit unit) {
        LocalDateTime
                sdt = parseStringToDateTime(ss),
                edt = parseStringToDateTime(es);
        LocalDate
                sd = sdt.toLocalDate(),
                ed = edt.toLocalDate();

        Set<Integer> yearSet = new HashSet<>();
        yearSet.add(sd.getYear());
        yearSet.add(ed.getYear());

        // 不包括周末和节假日,且节假日后的补班需要算作工作日。
        if (CollectionUtils.isEmpty(YEAR_HOLIDAY_MAP_SET)) {
            startMaintenanceYearHolidayConcurrentHashSet(yearSet);
        }

        // 判断是不是同一天
        boolean startFreeDay = isThisFreeDay(sd);
        boolean endFreeDay = isThisFreeDay(ed);
        if (Objects.equals(sd, ed)) {
            return startFreeDay ? 0 : unit.between(sdt, edt);
        }

        /*
         * 判断开始、结束日期是否为休息日(双休日<周六/周日>或节假日),
         * 是的话就将相应的日期往前或往后移,步长一天,直到这一天不是休息日为止。
         */
        int moveDays = 0;
        LocalDate calcSd = sd;
        LocalDate calcEd = ed;
        while (startFreeDay) {
            ++moveDays;
            calcSd = calcSd.minusDays(1); // 将日期往前一天
            startFreeDay = isThisFreeDay(calcSd); // 往前一天后再次判断
        }
        while (endFreeDay) {
            calcEd = calcEd.plusDays(1); // 将日期往后一天
            endFreeDay = isThisFreeDay(calcEd); // 往后一天后再次判断
        }
        moveDays = moveDays > 0 ? 1 : 0;

        // 计算时间差值
        LocalDateTime calcSdt = sdt;
        LocalDateTime salcEdt = edt;
        if (!sd.isEqual(calcSd)) {
            calcSdt = calcSd.atStartOfDay();
        }
        if (!ed.isEqual(calcEd)) {
            salcEdt = calcEd.atStartOfDay();
        }
        long differenceUnits = unit.between(calcSdt, salcEdt);
        long weekendHolidayDays = calculateWeekendHolidayDays(calcSdt.toLocalDate(), salcEdt.toLocalDate());
        long subDays = weekendHolidayDays + moveDays;

        if (ChronoUnit.DAYS == unit) {
            differenceUnits -= subDays;
        } else {
            differenceUnits -= subDays * (unit.between(
                    LocalTime.of(0, 0, 0),
                    LocalTime.of(23, 59, 59, 999999999)) + 1);
        }
        return differenceUnits;
    }

    /**
     * 判断当前日期是否为休息日
     *
     * @param date 日期
     * @return 是否休息日
     */
    private static boolean isThisFreeDay(LocalDate date) {
        int year = date.getYear();
        if (CollectionUtils.isEmpty(YEAR_HOLIDAY_MAP_SET) || CollectionUtils.isEmpty(YEAR_HOLIDAY_MAP_SET.get(year))) {
            YEAR_HOLIDAY_MAP_SET.put(year, obtainHolidayConcurrentHashSet(year));
        }
        Map<Boolean, ConcurrentHashSet<LocalDate>> holidayMapSet = YEAR_HOLIDAY_MAP_SET.get(year);
        Set<LocalDate>
                holidaySet = holidayMapSet.get(true),
                nonHolidaySet = holidayMapSet.get(false);

        DayOfWeek ofWeek = date.getDayOfWeek();
        return !nonHolidaySet.contains(date)
                && (ofWeek == DayOfWeek.SATURDAY || ofWeek == DayOfWeek.SUNDAY || holidaySet.contains(date));
    }

    /**
     * 生成并维护年份假期集合
     *
     * @param yearSet 年份集合
     */
    private static void startMaintenanceYearHolidayConcurrentHashSet(Set<Integer> yearSet) {
        Map<Integer, Map<Boolean, ConcurrentHashSet<LocalDate>>> yearHolidayMap = yearSet.stream()
                .collect(Collectors.toMap(
                        year -> year,
                        TimeUtil::obtainHolidayConcurrentHashSet,
                        (a, b) -> b)
                );
        if (CollectionUtils.isEmpty(YEAR_HOLIDAY_MAP_SET)) {
            YEAR_HOLIDAY_MAP_SET.putAll(yearHolidayMap);
        } else {
            Map<Integer, Map<Boolean, ConcurrentHashSet<LocalDate>>> freshYearMember = yearHolidayMap.entrySet().stream()
                    .filter(e -> !YEAR_HOLIDAY_MAP_SET.containsKey(e.getKey()))
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
            YEAR_HOLIDAY_MAP_SET.putAll(freshYearMember);
        }
    }

    /**
     * 获取某一年的假期集合
     *
     * @param year 年份
     * @return 假期集合
     */
    private static Map<Boolean, ConcurrentHashSet<LocalDate>> obtainHolidayConcurrentHashSet(Integer year) {
        Gson gson = new Gson();
        Type type = new TypeToken<HolidayTemplate>() {
        }.getType();
        String json = HttpUtil.getInstance().sendHttpGet(HOLIDAY_API + year);
        HolidayTemplate template = gson.fromJson(json, type);
        Map<String, HolidayTemplate.Content> holidayMap = template.getHoliday();

        Map<Boolean, Set<LocalDate>> originalMap = holidayMap.values().stream()
                .collect(Collectors.groupingBy(HolidayTemplate.Content::getHoliday,
                        Collectors.mapping(v -> parseStringToDateTime(v.getDate() + " 00:00:00.000").toLocalDate()
                                , Collectors.toSet())));

        // 转换 ConcurrentHashSet 进行返回
        return originalMap.entrySet().stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        entry -> new ConcurrentHashSet<>(entry.getValue()),
                        (set1, set2) -> {
                            set1.addAll(set2);
                            return set1;
                        }, ConcurrentHashMap::new
                ));
    }

    /**
     * 计算时间区间的休息日
     *
     * @param sd 开始时间
     * @param ed 结束时间
     * @return 期间休息天数
     */
    private static long calculateWeekendHolidayDays(LocalDate sd, LocalDate ed) {
        long weekendHolidayDays = 0;
        while (!sd.isAfter(ed)) {
            if (isThisFreeDay(sd)) {
                ++weekendHolidayDays;
            }
            sd = sd.plusDays(1);
        }
        return weekendHolidayDays;
    }

    /**
     * 解析不同格式的时间字符串
     *
     * @param timeString 时间字符串
     * @return 时间对象
     */
    public static LocalDateTime parseStringToDateTime(String timeString) {
        // 支持的时间格式数组,可以根据需要扩展
        String[] supportedFormats = {
                "yyyy-MM-dd HH:mm:ss",
                "yyyy/MM/dd HH:mm:ss",
                "yyyyMMdd HHmmss",
                "yyyy-MM-dd HH:mm:ss.SSS",
                "yyyy/MM/dd HH:mm:ss.SSS",
                "yyyyMMdd HHmmssSSS"
        };
        LocalDateTime dateTime = null;
        for (String format : supportedFormats) {
            try {
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
                dateTime = LocalDateTime.parse(timeString, formatter);
                // 如果成功解析,则跳出循环
                break;
            } catch (DateTimeParseException e) {
                // 如果解析失败,继续尝试下一个格式
            }
        }
        if (dateTime == null) {
            throw new RuntimeException("时间格式有误!");
        }
        return dateTime;
    }

    /**
     * 根据时间单位格式化时间
     *
     * @param timeInUnits 指定单位的时间数
     * @param unit        时间单位
     * @return 格式化时间
     */
    private static String formatTimeDifferenceUnit(long timeInUnits, ChronoUnit unit) {
        Duration duration = Duration.of(timeInUnits, unit);
        long millis = duration.toMillis();
        return formatTimeDifferenceMills(millis, unit);
    }

    /**
     * 格式化时间 -> xx天xx时xx分xx秒xx毫秒 字符串
     *
     * @param mills 毫秒数
     * @return 格式化时间
     */
    private static String formatTimeDifferenceMills(long mills, ChronoUnit unit) {
        int ss = 1000;
        int mi = ss * 60;
        int hh = mi * 60;
        int dd = hh * 24;

        long day = mills / dd;
        long hou = mills % dd / hh;
        long min = mills % hh / mi;
        long sec = mills % mi / ss;
        long mil = mills % ss;
        return formatDurationSplice(day, hou, min, sec, mil, unit);
    }


    private static String formatDurationSplice(long days, long hours, long minutes, long seconds, long mills) {
        if (days == 0 && hours == 0 && minutes == 0 && seconds == 0 && mills == 0) {
            return "0毫秒";
        }

        Long[] array = {days, hours, minutes, seconds, mills};

        // 去除头尾0
        int start = 0;
        while (start < array.length && array[start] == 0) {
            array[start] = null;
            start++;
        }
        int end = array.length - 1;
        while (end >= 0 && array[end] == 0) {
            array[end] = null;
            end--;
        }

        StringBuilder builder = new StringBuilder();
        Long day = array[0];
        Long hou = array[1];
        Long min = array[2];
        Long sec = array[3];
        Long mil = array[4];

        if (day != null) {
            builder.append(day).append("天");
        }
        if (hou != null) {
            builder.append(hou).append("时");
        }
        if (min != null) {
            builder.append(min).append("分");
        }
        if (sec != null) {
            builder.append(sec).append("秒");
        }
        if (mil != null) {
            builder.append(mil).append("毫秒");
        }
        return builder.toString();
    }

    /**
     * 时间格式化,去除头尾0,拼接。
     *
     * @param days    天数
     * @param hours   小时数
     * @param minutes 分钟数
     * @param seconds 秒数
     * @param mills   毫秒数
     * @param unit    时间单位
     * @return 时间
     */
    private static String formatDurationSplice(long days, long hours, long minutes, long seconds, long mills, ChronoUnit unit) {
        if (days == 0 && hours == 0 && minutes == 0 && seconds == 0 && mills == 0) {
            switch (unit) {
                case DAYS:
                    return "0天";
                case HOURS:
                    return "0时";
                case MINUTES:
                    return "0分";
                case SECONDS:
                    return "0秒";
                case MILLIS:
                    return "0毫秒";
                default:
                    return "0秒";
            }
        }

        Long[] array = {days, hours, minutes, seconds, mills};

        // 去除头尾0
        int start = 0;
        while (start < array.length && array[start] == 0) {
            array[start] = null;
            start++;
        }
        int end = array.length - 1;
        while (end >= 0 && array[end] == 0) {
            array[end] = null;
            end--;
        }

        StringBuilder builder = new StringBuilder();
        Long day = array[0];
        Long hou = array[1];
        Long min = array[2];
        Long sec = array[3];
        Long mil = array[4];

        if (day != null) {
            builder.append(day).append("天");
        }
        if (hou != null) {
            builder.append(hou).append("时");
        }
        if (min != null) {
            builder.append(min).append("分");
        }
        if (sec != null) {
            builder.append(sec).append("秒");
        }
        if (mil != null) {
            builder.append(mil).append("毫秒");
        }
        return builder.toString();
    }

    @Data
    static class HolidayTemplate implements Serializable {
        private static final long serialVersionUID = 7004590945038461126L;
        private Integer code;
        private Map<String, Content> holiday;

        @Data
        static class Content implements Serializable {
            private static final long serialVersionUID = 5689990994927562337L;
            private Boolean holiday;
            private String name;
            private Integer wage;
            private String date;
            private Integer rest;
        }
    }
}

posted @ 2023-10-23 13:53  溫柔の風  阅读(7)  评论(0编辑  收藏  举报