参考:https://www.cnblogs.com/DJOSIMON/p/17782244.html
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;
}
}
}