package com.dates.utils;
import cn.hutool.core.collection.CollUtil;
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.WeekFields;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author chengzb-c
* @date 2025年02月11日17:27:56
* @doc 日期工具类
*/
@Slf4j
public class DateUtils {
/**
* 计算给定日期范围内的周数,并根据每周包含的天数来确定是否将该周纳入考核。
* 返回纳入考核的周数。
*
* @param startDate 统计周期的起始日期
* @param endDate 统计周期的结束日期
* @param minDaysInWeek 纳入考核的最小天数
* @return 返回纳入考核的周数
*/
public static int calculateWeeksInPeriod(LocalDate startDate, LocalDate endDate, int minDaysInWeek) {
int weeksInPeriod = 0;
LocalDate currentDate = startDate;
while (!currentDate.isAfter(endDate)) {
LocalDate nextWeek = currentDate.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
int daysInWeek = Period.between(currentDate, nextWeek).getDays();
if (daysInWeek >= minDaysInWeek) {
weeksInPeriod++;
}
currentDate = nextWeek;
}
return weeksInPeriod;
}
/**
* 计算给定日期范围内的周数,并根据每周包含的天数来确定是否将该周纳入考核。
* 返回纳入考核的具体天数列表。
*
* @param startDate 统计周期的起始日期
* @param endDate 统计周期的结束日期
* @param minDaysInWeek 纳入考核的最小天数
* @return 纳入考核的具体天数列表
*/
public static List<List<LocalDate>> calculateWeeksInPeriodByDay(LocalDate startDate, LocalDate endDate, int minDaysInWeek) {
LocalDate currentDate = startDate;
List<List<LocalDate>> includedDays = new ArrayList<>();
while (!currentDate.isAfter(endDate)) {
LocalDate nextWeek = currentDate.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
//如果是最后一天是5天
if (nextWeek.isAfter(endDate)) {
nextWeek = endDate.plusDays(1);
}
int daysInWeek = Period.between(currentDate, nextWeek).getDays();
if (daysInWeek >= minDaysInWeek) {
List<LocalDate> daysInPeriod = Stream.iterate(currentDate, date -> date.plusDays(1))
.limit(daysInWeek)
.collect(Collectors.toList());
includedDays.add(daysInPeriod);
}
currentDate = nextWeek;
}
return includedDays;
}
/**
* 将连续的日期列表转换为多个时间区间
* @param dates 日期列表(需已排序)
* @return 包含开始和结束时间的时间区间列表
*/
public static List<DayRange> convertToDateRanges(List<LocalDate> dates) {
List<DayRange> ranges = new ArrayList<>();
if (CollUtil.isEmpty(dates)) {
return ranges;
}
// 对日期进行排序
dates.sort(LocalDate::compareTo);
LocalDate start = dates.get(0);
LocalDate end = start;
for (int i = 1; i < dates.size(); i++) {
LocalDate current = dates.get(i);
// 如果当前日期与上一个日期连续
if (current.equals(end.plusDays(1))) {
end = current;
} else {
// 保存当前区间
DayRange dayRange = new DayRange();
dayRange.setStartDate(start);
dayRange.setEndDate(end);
ranges.add(dayRange);
// 开始新的区间
start = current;
end = current;
}
}
// 添加最后一个区间
DayRange dayRange = new DayRange();
dayRange.setStartDate(start);
dayRange.setEndDate(end);
ranges.add(dayRange);
return ranges;
}
/**
* @param date 日期
* @param minDaysInWeek 周内的最小天数
* @return {@link List<List<LocalDate>>}
*/
public static List<List<LocalDate>> calculateWeeksInPeriodByDay(List<LocalDate> date, int minDaysInWeek) {
if (CollUtil.isEmpty(date)) {
return Collections.emptyList();
}
List<List<LocalDate>> days = new ArrayList<>();
List<DayRange> dayRanges = convertToDateRanges(date);
for (DayRange dayRange : dayRanges) {
days.addAll(calculateWeeksInPeriodByDay(dayRange.getStartDate(), dayRange.getEndDate(), minDaysInWeek));
}
return days;
}
/**
* 时间区间排除时间段
* @param startDate
* @param endDate
* @param excludedDayRanges
* @return
*/
public static List<LocalDate> excludeHolidays(LocalDate startDate, LocalDate endDate, List<DayRange> excludedDayRanges) {
List<LocalDate> result = new ArrayList<>();
LocalDate current = startDate;
// 遍历整个日期区间
while (!current.isAfter(endDate)) {
boolean isExcluded = false;
// 检查当前日期是否在排除区间内
for (DayRange range : excludedDayRanges) {
if (!current.isBefore(range.getStartDate()) && !current.isAfter(range.getEndDate())) {
isExcluded = true;
break;
}
}
// 如果不在排除区间内,则加入结果列表
if (!isExcluded) {
result.add(current);
}
current = current.plusDays(1);
}
return result;
}
/**
* 从已经纳入考核的天数列表中排除指定的一些天。
*
* @param includedDays 已经纳入考核的天数列表
* @param excludedDayStart 需要排除的天数开始
* @param excludedDayEnd 需要排除的天数结束
* @return 排除指定天数后的考核天数列表
*/
public static List<List<LocalDate>> excludeDays(List<List<LocalDate>> includedDays, LocalDate excludedDayStart, LocalDate excludedDayEnd) {
if (excludedDayStart == null || excludedDayEnd == null) {
return includedDays;
}
excludedDayEnd = excludedDayEnd.plusDays(1);
List<LocalDate> excludedDays = Stream.iterate(excludedDayStart, date -> date.plusDays(1))
.limit(excludedDayStart.until(excludedDayEnd, ChronoUnit.DAYS) - 1)
.collect(Collectors.toList());
log.info("[人员履职]:过滤:{}", excludedDays);
List<List<LocalDate>> resultDateList = new ArrayList<>();
for (List<LocalDate> localDates : includedDays) {
if (!excludedDays.contains(localDates.get(0))) {
resultDateList.add(localDates);
}
}
log.info("[人员履职]:过滤扣除后:{}", resultDateList);
return resultDateList;
}
/**
* 计算给定日期范围内的月度考核情况。
* 若当前考核时间包含天数大于该月总天数的1/2,则该月计入考核;
* 若包含天数小于等于该月总天数的1/2,则该月不计入考核。
*
* @param startDate 统计周期的起始日期
* @param endDate 统计周期的结束日期
* @return 纳入考核的月份列表
*/
public static List<List<LocalDate>> calculateMonthsInPeriod(LocalDate startDate, LocalDate endDate) {
List<List<LocalDate>> includedMonths = new ArrayList<>();
LocalDate currentDate = startDate;
while (!currentDate.isAfter(endDate)) {
int daysInMonth = currentDate.lengthOfMonth();
LocalDate nextMonth = currentDate.plusMonths(1);
LocalDate startOfDay = nextMonth.atStartOfDay().toLocalDate();
if (startOfDay.isAfter(endDate)) {
nextMonth = endDate.plusDays(1);
}
int daysInPeriod = (int) ChronoUnit.DAYS.between(currentDate, nextMonth);
int halfDaysInMonth = (daysInMonth + 1) / 2;
if (daysInPeriod > halfDaysInMonth) {
List<LocalDate> dateByMonth = Stream.iterate(currentDate, date -> date.plusDays(1))
.limit(daysInPeriod)
.collect(Collectors.toList());
includedMonths.add(dateByMonth);
}
currentDate = currentDate.plusMonths(1);
}
return includedMonths;
}
/**
* 计算给定日期范围内的月度。
* 若当前考核时间包含天数大于该月总天数的1/2,则该月计入考核;
* 若包含天数小于等于该月总天数的1/2,则该月不计入考核。
*
* @param startDate 统计周期的起始日期
* @param endDate 统计周期的结束日期
* @return 纳入考核的月份列表
*/
public static List<YearMonth> getYearMonthsWithMoreThanHalfDaysInRange(LocalDate startDate, LocalDate endDate) {
List<YearMonth> result = new ArrayList<>();
// 确保startDate在endDate之前
if (startDate.isAfter(endDate)) {
return result;
}
// 使用YearMonth类来遍历月份
YearMonth startYearMonth = YearMonth.from(startDate);
YearMonth endYearMonth = YearMonth.from(endDate);
endYearMonth = endYearMonth.plusMonths(1); // 加一个月以确保包含endDate所在的月份
// 遍历起始月份到结束月份之前的所有月份
for (YearMonth yearMonth = startYearMonth; !yearMonth.isAfter(endYearMonth); yearMonth = yearMonth.plusMonths(1)) {
// 获取当前月份的第一天和最后一天
LocalDate firstDayOfMonth = yearMonth.atDay(1);
LocalDate lastDayOfMonth = yearMonth.atEndOfMonth();
// 计算给定日期范围内当前月份的天数
long daysInRange = ChronoUnit.DAYS.between(
startDate.isBefore(firstDayOfMonth) ? firstDayOfMonth : startDate,
endDate.isAfter(lastDayOfMonth) ? lastDayOfMonth : endDate) + 1;
// 获取当前月份的总天数
int daysInMonth = yearMonth.lengthOfMonth();
// 检查在范围内的天数是否大于月份天数的一半
if (daysInRange > daysInMonth / 2l) {
// 如果是,则添加该年和月到结果列表中
// 使用格式 "YYYY-MM"
result.add(yearMonth);
}
}
return result;
}
// 根据季度和年份获取该季度的所有月份
public static List<YearMonth> getYearMonths(Integer year, Integer quarter) {
List<YearMonth> result = new ArrayList<>();
switch (quarter) {
case 1:
result.add(YearMonth.of(year, 1));
result.add(YearMonth.of(year, 2));
result.add(YearMonth.of(year, 3));
break;
case 2:
result.add(YearMonth.of(year, 4));
result.add(YearMonth.of(year, 5));
result.add(YearMonth.of(year, 6));
break;
case 3:
result.add(YearMonth.of(year, 7));
result.add(YearMonth.of(year, 8));
result.add(YearMonth.of(year, 9));
break;
case 4:
result.add(YearMonth.of(year, 10));
result.add(YearMonth.of(year, 11));
result.add(YearMonth.of(year, 12));
break;
default:
break;
}
return result;
}
/**
* 计算给定日期范围内的季度考核情况。
* 若当前考核时间包含天数大于该月总天数的1/2,则该月计入考核;
* 若包含天数小于等于该月总天数的1/2,则该月不计入考核。
*
* @param startDate 统计周期的起始日期
* @param endDate 统计周期的结束日期
* @return 纳入考核的月份列表
*/
public static List<List<LocalDate>> calculateQuarterInPeriod(LocalDate startDate, LocalDate endDate) {
List<List<LocalDate>> includedMonths = new ArrayList<>();
LocalDate currentDate = startDate;
while (!currentDate.isAfter(endDate)) {
int daysInQuarter = getDaysInQuarter(currentDate); // 30 * 3
LocalDate nextQuarter = plusQuarter(currentDate);
LocalDate startOfDay = nextQuarter.atStartOfDay().toLocalDate();
if (startOfDay.isAfter(endDate)) {
nextQuarter = endDate.plusDays(1);
}
int daysInPeriod = (int) ChronoUnit.DAYS.between(currentDate, nextQuarter); // 10-10 - 12-31
int halfDaysInMonth = (daysInQuarter + 1) / 2;
if (daysInPeriod > halfDaysInMonth) {
List<LocalDate> dateByMonth = Stream.iterate(currentDate, date -> date.plusDays(1))
.limit(daysInPeriod)
.collect(Collectors.toList());
includedMonths.add(dateByMonth);
}
currentDate = nextQuarter;
}
return includedMonths;
}
/**
* 获取当前季度的总天数
* @param localDate 当前日期
* @return 当前季度的总天数
*/
public static int getDaysInQuarter(LocalDate localDate) {
int quarter = getQuarter(localDate);
int year = localDate.getYear();
switch (quarter) {
case 1:
return YearMonth.of(year, 1).lengthOfMonth()
+ YearMonth.of(year, 2).lengthOfMonth()
+ YearMonth.of(year, 3).lengthOfMonth();
case 2:
return YearMonth.of(year, 4).lengthOfMonth()
+ YearMonth.of(year, 5).lengthOfMonth()
+ YearMonth.of(year, 6).lengthOfMonth();
case 3:
return YearMonth.of(year, 7).lengthOfMonth()
+ YearMonth.of(year, 8).lengthOfMonth()
+ YearMonth.of(year, 9).lengthOfMonth();
case 4:
return YearMonth.of(year, 10).lengthOfMonth()
+ YearMonth.of(year, 11).lengthOfMonth()
+ YearMonth.of(year, 12).lengthOfMonth();
default:
throw new IllegalArgumentException("Invalid quarter: " + quarter);
}
}
private static LocalDate plusQuarter(LocalDate currentDate) {
int monthValue = currentDate.getMonthValue();
int year = currentDate.getYear();
switch (monthValue) {
case 1:
case 2:
case 3:
return LocalDate.of(year, 4, 1);
case 4:
case 5:
case 6:
return LocalDate.of(year, 7, 1);
case 7:
case 8:
case 9:
return LocalDate.of(year, 10, 1);
case 10:
case 11:
case 12:
return LocalDate.of(year+1, 1, 1);
default:
throw new IllegalStateException("Unexpected month value: " + monthValue);
}
}
/**
* 判断给定的时间是否在给定的日期范围内。 整天维度
* @param days
* @param time
* @return
*/
public static Boolean isContains(List<List<LocalDate>> days, Long time) {
List<Long> dayList = days.stream().flatMap(List::stream).
map(DateUtils::getTimestamp)
.collect(Collectors.toList());
return dayList.contains(DateUtils.getTimestamp(time));
}
/**
* 获取档期日期的季度
*
* @param localDate 本地日期
* @return int
*/
public static int getQuarter(LocalDate localDate) {
int month = localDate.getMonthValue();
return (month - 1) / 3 + 1;
}
/**
* 获取指定日期是当月的第几周
* @param localDate 指定日期
* @return 当月的第几周
*/
public static int getWeekOfMonth(LocalDate localDate) {
// 使用 ISO 标准的周定义,周一为一周的开始
WeekFields weekFields = WeekFields.ISO;
// 获取该日期是当月的第几周
return localDate.get(weekFields.weekOfMonth());
}
/**
* 定义一个方法用于将 Date 转换为 LocalDate
*
* @param date
* @return
*/
public static LocalDate convertDateToLocalDate(Date date) {
// 将 Date 转换为 Instant
Instant instant = date.toInstant();
// 获取系统默认时区
ZoneId zoneId = ZoneId.systemDefault();
// 将 Instant 转换为 LocalDate
return instant.atZone(zoneId).toLocalDate();
}
/**
* 获取两个时间周期内的所有日期
* @param startDate 开始日期
* @param endDate 结束日期
* @return 包含所有日期的列表
*/
public static List<LocalDate> getAllDaysBetween(LocalDate startDate, LocalDate endDate) {
List<LocalDate> dates = new ArrayList<>();
LocalDate currentDate = startDate;
while (!currentDate.isAfter(endDate)) {
dates.add(currentDate);
currentDate = currentDate.plusDays(1);
}
return dates;
}
// ==== 按照周计算=======
// public static void main(String[] args) {
// int minDaysInWeek = 5;
// LocalDate startDate = LocalDate.of(2025, 2, 3);
// LocalDate endDate = LocalDate.of(2025, 2, 28);
// // 假设需要排除的天数
// LocalDate excludedDayStart = LocalDate.of(2025, 2, 10);
// LocalDate excludedDayEnd = LocalDate.of(2025, 2, 16);
//
// int weeksInPeriod = calculateWeeksInPeriod(startDate, endDate, minDaysInWeek);
// System.out.println("纳入考核的周数: " + weeksInPeriod);
//
// List<List<LocalDate>> weeksInPeriodByDay = calculateWeeksInPeriodByDay(startDate, endDate, minDaysInWeek);
// List<List<LocalDate>> finalIncludedDays = excludeDays(weeksInPeriodByDay, excludedDayStart, excludedDayEnd);
//
// System.out.println("纳入考核的天: " + weeksInPeriodByDay);
// System.out.println("排除指定天数后的考核天数: " + finalIncludedDays);
// }
/**
* 将 LocalDate 转换为 Date
* @param localDate 要转换的 LocalDate 对象
* @return 转换后的 Date 对象
*/
public static Date convertToDate(LocalDate localDate) {
java.time.ZonedDateTime zonedDateTime = localDate.atStartOfDay(ZoneId.systemDefault());
return Date.from(zonedDateTime.toInstant());
}
/**
* 格式化日期
* @param localDate
* @return
*/
public static String localDateToDateString(LocalDate localDate) {
return localDate.format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
}
/**
* 格式化日期
* @param milliTime
* @return
*/
public static String milliTimeToDateString(Long milliTime) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(milliTime), ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
}
/**
* 格式化日期
* @param milliTime
* @return
*/
public static LocalDate milliTimeToLocalDate(Long milliTime) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(milliTime), ZoneId.systemDefault()).toLocalDate();
}
/**
* 格式化日期
*/
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy.MM.dd");
public static String dateToDateString(Date date) {
return SIMPLE_DATE_FORMAT.format(date);
}
/*
* 将时间段进行合并
* 入参: [(25-01-01 - 25-01-09), (25-01-08 - 25-01-15), (25-01-20 - 25-01-30)]
* 结果: [(25-01-01 - 25-01-15), (25-01-20 - 25-01-30)]
* 将数据段也根据时间进行合并.
*/
public static List<ProjectDateRangeProps> mergeIntervalsByDate(List<ProjectDateRangeProps> intervals) {
if (intervals == null || intervals.isEmpty()) {
return Collections.emptyList();
}
// 按 projectId 分组
Map<Long, List<ProjectDateRangeProps>> intervalsByProject = intervals.stream()
.collect(Collectors.groupingBy(ProjectDateRangeProps::getProjectId));
// 对每个 projectId 的时间间隔进行合并
List<ProjectDateRangeProps> mergedIntervals = new ArrayList<>();
for (Map.Entry<Long, List<ProjectDateRangeProps>> entry : intervalsByProject.entrySet()) {
List<ProjectDateRangeProps> projectIntervals = entry.getValue();
mergedIntervals.addAll(mergeDate(projectIntervals));
}
return mergedIntervals;
}
public static List<ProjectDateRangeProps> mergeDate(List<ProjectDateRangeProps> singleProjectScoreCycles) {
if (CollUtil.isEmpty(singleProjectScoreCycles)) {
return Collections.emptyList();
}
// 对区间进行排序
singleProjectScoreCycles.sort(Comparator.comparing(ProjectDateRangeProps::getStartDate));
List<ProjectDateRangeProps> mergedIntervals = new ArrayList<>();
ProjectDateRangeProps current = singleProjectScoreCycles.get(0);
for (int i = 1; i < singleProjectScoreCycles.size(); i++) {
ProjectDateRangeProps next = singleProjectScoreCycles.get(i);
Calendar nextStartCal = Calendar.getInstance();
nextStartCal.setTime(next.getStartDate());
Calendar currentEndCal = Calendar.getInstance();
currentEndCal.setTime(current.getEndDate());
currentEndCal.add(Calendar.DAY_OF_YEAR, 1);
// 比较日期
if (!nextStartCal.after(currentEndCal)) {
Calendar currentEndCalForCompare = Calendar.getInstance();
currentEndCalForCompare.setTime(current.getEndDate());
Calendar nextEndCal = Calendar.getInstance();
nextEndCal.setTime(next.getEndDate());
Date newEndDate = currentEndCalForCompare.after(nextEndCal) ? current.getEndDate() : next.getEndDate();
current.setEndDate(newEndDate);
} else {
mergedIntervals.add(current);
current = next;
}
}
mergedIntervals.add(current);
return mergedIntervals;
}
/**
* 合并日期间隔
*
* @param dateRangeProps 日期范围道具
* @return {@link List<DateRangeProps>}
*/
public static List<DateRangeProps> mergeIntervals(List<DateRangeProps> dateRangeProps) {
if (CollUtil.isEmpty(dateRangeProps)) {
return Collections.emptyList();
}
// 对区间进行排序
dateRangeProps.sort(Comparator.comparing(DateRangeProps::getStartDate));
List<DateRangeProps> mergedIntervals = new ArrayList<>();
DateRangeProps current = dateRangeProps.get(0);
for (int i = 1; i < dateRangeProps.size(); i++) {
DateRangeProps next = dateRangeProps.get(i);
Calendar nextStartCal = Calendar.getInstance();
nextStartCal.setTime(next.getStartDate());
Calendar currentEndCal = Calendar.getInstance();
currentEndCal.setTime(current.getEndDate());
currentEndCal.add(Calendar.DAY_OF_YEAR, 1);
// 比较日期
if (!nextStartCal.after(currentEndCal)) {
Calendar currentEndCalForCompare = Calendar.getInstance();
currentEndCalForCompare.setTime(current.getEndDate());
Calendar nextEndCal = Calendar.getInstance();
nextEndCal.setTime(next.getEndDate());
Date newEndDate = currentEndCalForCompare.after(nextEndCal) ? current.getEndDate() : next.getEndDate();
current.setEndDate(newEndDate);
} else {
mergedIntervals.add(current);
current = next;
}
}
mergedIntervals.add(current);
return mergedIntervals;
}
/**
* 获取当天(整天 00:00:00) 的时间戳
* @param localDate
* @return
*/
public static long getTimestamp(LocalDate localDate) {
return localDate.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() / CommonConsts.ONE_DAY_IN_MILLIS * CommonConsts.ONE_DAY_IN_MILLIS;
}
/**
* 获取当天(整天 00:00:00) 的时间戳
* @param timestamp
* @return
*/
public static long getTimestamp(Long timestamp) {
return timestamp / CommonConsts.ONE_DAY_IN_MILLIS * CommonConsts.ONE_DAY_IN_MILLIS;
}
}
public static void main(String[] args) {
log.info("[会议管理]:WEEKLY:days");
LocalDate startDate = LocalDate.of(2024, 10, 1);
LocalDate endDate = LocalDate.of(2024, 12, 27);
List<DayRange> excludedDayRanges = new ArrayList<>();
DayRange dayRange2 = new DayRange();
dayRange2.setStartDate(LocalDate.of(2024, 10, 1));
dayRange2.setEndDate(LocalDate.of(2024, 10, 7));
excludedDayRanges.add(dayRange2);
DayRange dayRange = new DayRange();
dayRange.setStartDate(LocalDate.of(2024,10,15));
dayRange.setEndDate(LocalDate.of(2024, 10, 28));
excludedDayRanges.add(dayRange);
DayRange dayRange1 = new DayRange();
dayRange1.setStartDate(LocalDate.of(2024, 11, 5));
dayRange1.setEndDate(LocalDate.of(2024, 12, 23));
excludedDayRanges.add(dayRange1);
List<LocalDate> dateList = DateUtils.excludeHolidays(startDate, endDate, excludedDayRanges);
log.info("[会议管理]:WEEKLY:排除后的时间:{}", dateList);
List<List<LocalDate>> days = DateUtils.calculateWeeksInPeriodByDay(dateList, CommonConsts.MIN_DAYS_IN_WEEK);
log.info("[会议管理]:WEEKLY:按周过滤的时间区间:{}", days);
}
````
Q.E.D.