diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/date/DateUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/date/DateUtil.java index ce0387b6bc29fcd83fc10f25d86dbb72b07b3818..5b4dfa94009cfe3eb30b6f44abfa7324c2ad8b39 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/date/DateUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/date/DateUtil.java @@ -41,6 +41,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * 日期时间工具类 @@ -2050,4 +2051,241 @@ public class DateUtil { public static int getLastDayOfMonth(final Date date) { return date(date).getLastDayOfMonth(); } + + /** + * 时间分片,将指定的时间区间按照指定的间隔切分为若干个时间片段 + *

+ * 例如: 将2000-01-01至2000-02-01的时间区间按照1天的间隔切分,结果为: + * [{ + * "start": "2000-01-01", + * "end": "2000-01-02" + * }, { + * "start": "2000-01-02", + * "end": "2000-01-03" + * }, ..., { + * "start": "2000-01-31", + * "end": "2000-02-01" + * }] + * + * @param startDate 起始时间 + * @param endDate 结束时间 + * @param dateField 切分的时间单位,例如:{@link DateField#DAY_OF_YEAR} + * @param step 切分的步长,例如:1 + * @return 切分后的时间片段列表,键值对中的键为片段开始时间,值为片段结束时间 + * @since 6.0.0 + */ + public static List> slice(final Date startDate, final Date endDate, final DateField dateField, final int step) { + if (startDate == null || endDate == null || dateField == null || step <= 0) { + return ListUtil.empty(); + } + + // 计算总的时间间隔(以指定单位为准) + long totalUnits = calculateTotalUnits(startDate, endDate, dateField); + // 计算需要分片的数量 + int sliceCount = (int) Math.ceil((double) totalUnits / step); + + // 使用Stream生成分片 + return Stream.iterate(0, i -> i + 1) + .limit(sliceCount) + .map(i -> { + // 计算当前片段的开始时间 + Date sliceStart = i == 0 ? startDate : offset(startDate, dateField, i * step); + // 计算当前片段的结束时间 + Date sliceEnd = offset(sliceStart, dateField, step); + // 确保结束时间不超过总的结束时间 + if (sliceEnd.after(endDate)) { + sliceEnd = endDate; + } + + // 创建并返回时间片段 + return createTimeSlice(sliceStart, sliceEnd); + }) + // 过滤掉无效片段(开始时间已经超过或等于结束时间) + .filter(slice -> ((Date) slice.get("start")).before((Date) slice.get("end"))) + .collect(Collectors.toList()); + } + + /** + * 计算两个日期之间的时间单位数量 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param dateField 日期字段 + * @return 单位数量 + */ + private static long calculateTotalUnits(Date startDate, Date endDate, DateField dateField) { + switch (dateField) { + case YEAR: + return betweenYear(startDate, endDate, false) + 1; + case MONTH: + return betweenMonth(startDate, endDate, false) + 1; + case WEEK_OF_YEAR: + case WEEK_OF_MONTH: + return betweenWeek(startDate, endDate, false) + 1; + case DAY_OF_YEAR: + case DAY_OF_MONTH: + case DAY_OF_WEEK: + return betweenDay(startDate, endDate, false) + 1; + case HOUR: + case HOUR_OF_DAY: + return between(startDate, endDate, DateUnit.HOUR) + 1; + case MINUTE: + return between(startDate, endDate, DateUnit.MINUTE) + 1; + case SECOND: + return between(startDate, endDate, DateUnit.SECOND) + 1; + case MILLISECOND: + return between(startDate, endDate, DateUnit.MS) + 1; + default: + return betweenDay(startDate, endDate, false) + 1; + } + } + + /** + * 创建时间片段 + * + * @param start 开始时间 + * @param end 结束时间 + * @return 时间片段Map + */ + private static Map createTimeSlice(Date start, Date end) { + return new LinkedHashMap(2) {{ + put("start", start); + put("end", end); + }}; + } + + /** + * 时间分片,将指定的时间区间按照指定的天数间隔切分为若干个时间片段 + *

+ * 例如: 将2000-01-01至2000-02-01的时间区间按照1天的间隔切分,结果为: + * [{ + * "start": "2000-01-01", + * "end": "2000-01-02" + * }, { + * "start": "2000-01-02", + * "end": "2000-01-03" + * }, ..., { + * "start": "2000-01-31", + * "end": "2000-02-01" + * }] + * + * @param startDate 起始时间 + * @param endDate 结束时间 + * @param days 切分的天数步长,例如:1 + * @return 切分后的时间片段列表,键值对中的键为片段开始时间,值为片段结束时间 + * @since 6.0.0 + */ + public static List> sliceByDay(final Date startDate, final Date endDate, final int days) { + return slice(startDate, endDate, DateField.DAY_OF_YEAR, days); + } + + /** + * 时间分片,将指定的时间区间按照指定的小时间隔切分为若干个时间片段 + * + * @param startDate 起始时间 + * @param endDate 结束时间 + * @param hours 切分的小时步长,例如:1 + * @return 切分后的时间片段列表,键值对中的键为片段开始时间,值为片段结束时间 + * @since 6.0.0 + */ + public static List> sliceByHour(final Date startDate, final Date endDate, final int hours) { + return slice(startDate, endDate, DateField.HOUR_OF_DAY, hours); + } + + /** + * 时间分片,将指定的时间区间按照指定的分钟间隔切分为若干个时间片段 + * + * @param startDate 起始时间 + * @param endDate 结束时间 + * @param minutes 切分的分钟步长,例如:30 + * @return 切分后的时间片段列表,键值对中的键为片段开始时间,值为片段结束时间 + * @since 6.0.0 + */ + public static List> sliceByMinute(final Date startDate, final Date endDate, final int minutes) { + return slice(startDate, endDate, DateField.MINUTE, minutes); + } + + /** + * 时间分片,将指定的时间区间按照指定的间隔切分为若干个时间片段,并按照指定的格式输出时间字符串 + *

+ * 例如: 将2000-01-01至2000-02-01的时间区间按照1天的间隔切分,并使用yyyy-MM-dd格式化时间,结果为: + * [{ + * "start": "2000-01-01", + * "end": "2000-01-02" + * }, { + * "start": "2000-01-02", + * "end": "2000-01-03" + * }, ..., { + * "start": "2000-01-31", + * "end": "2000-02-01" + * }] + * + * @param startDate 起始时间 + * @param endDate 结束时间 + * @param dateField 切分的时间单位,例如:{@link DateField#DAY_OF_YEAR} + * @param step 切分的步长,例如:1 + * @param format 时间格式,例如:"yyyy-MM-dd"、"yyyy-MM-dd HH:mm:ss" + * @return 切分后的时间片段列表,键值对中的键为片段开始时间字符串,值为片段结束时间字符串 + * @since 6.0.0 + */ + public static List> sliceAndFormat(final Date startDate, final Date endDate, final DateField dateField, final int step, final String format) { + if (startDate == null || endDate == null || dateField == null || step <= 0 || format == null) { + return ListUtil.empty(); + } + + // 先获取日期分片 + List> dateSlices = slice(startDate, endDate, dateField, step); + + // 将日期格式化为字符串 + return dateSlices.stream() + .map(slice -> { + Map formattedSlice = new LinkedHashMap<>(2); + formattedSlice.put("start", format(slice.get("start"), format)); + formattedSlice.put("end", format(slice.get("end"), format)); + return formattedSlice; + }) + .collect(Collectors.toList()); + } + + /** + * 时间分片,将指定的时间区间按照指定的天数间隔切分为若干个时间片段,并按照指定的格式输出时间字符串 + * + * @param startDate 起始时间 + * @param endDate 结束时间 + * @param days 切分的天数步长,例如:1 + * @param format 时间格式,例如:"yyyy-MM-dd"、"yyyy-MM-dd HH:mm:ss" + * @return 切分后的时间片段列表,键值对中的键为片段开始时间字符串,值为片段结束时间字符串 + * @since 6.0.0 + */ + public static List> sliceByDayAndFormat(final Date startDate, final Date endDate, final int days, final String format) { + return sliceAndFormat(startDate, endDate, DateField.DAY_OF_YEAR, days, format); + } + + /** + * 时间分片,将指定的时间区间按照指定的小时间隔切分为若干个时间片段,并按照指定的格式输出时间字符串 + * + * @param startDate 起始时间 + * @param endDate 结束时间 + * @param hours 切分的小时步长,例如:1 + * @param format 时间格式,例如:"yyyy-MM-dd HH"、"yyyy-MM-dd HH:mm:ss" + * @return 切分后的时间片段列表,键值对中的键为片段开始时间字符串,值为片段结束时间字符串 + * @since 6.0.0 + */ + public static List> sliceByHourAndFormat(final Date startDate, final Date endDate, final int hours, final String format) { + return sliceAndFormat(startDate, endDate, DateField.HOUR_OF_DAY, hours, format); + } + + /** + * 时间分片,将指定的时间区间按照指定的分钟间隔切分为若干个时间片段,并按照指定的格式输出时间字符串 + * + * @param startDate 起始时间 + * @param endDate 结束时间 + * @param minutes 切分的分钟步长,例如:30 + * @param format 时间格式,例如:"yyyy-MM-dd HH:mm"、"yyyy-MM-dd HH:mm:ss" + * @return 切分后的时间片段列表,键值对中的键为片段开始时间字符串,值为片段结束时间字符串 + * @since 6.0.0 + */ + public static List> sliceByMinuteAndFormat(final Date startDate, final Date endDate, final int minutes, final String format) { + return sliceAndFormat(startDate, endDate, DateField.MINUTE, minutes, format); + } } diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/date/DateUtilSliceFormatTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/date/DateUtilSliceFormatTest.java new file mode 100644 index 0000000000000000000000000000000000000000..790a4dbe99bac1929f1b88e92932ed69b3a31d9a --- /dev/null +++ b/hutool-core/src/test/java/org/dromara/hutool/core/date/DateUtilSliceFormatTest.java @@ -0,0 +1,196 @@ +package org.dromara.hutool.core.date; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * DateUtil时间分片格式化功能测试用例 + * + * @author Hutool + */ +public class DateUtilSliceFormatTest { + + /** + * 测试按天分片并格式化 + */ + @Test + public void testSliceByDayAndFormat() { + // 准备测试数据 + Date startDate = DateUtil.parse("2000-01-01"); + Date endDate = DateUtil.parse("2000-01-05"); + + // 按1天分片,并使用yyyy-MM-dd格式 + List> slices = DateUtil.sliceByDayAndFormat(startDate, endDate, 1, "yyyy-MM-dd"); + + // 验证结果 + Assertions.assertEquals(4, slices.size()); + + System.out.println(slices); + + // 验证第一个分片 + Map firstSlice = slices.get(0); + Assertions.assertEquals("2000-01-01", firstSlice.get("start")); + Assertions.assertEquals("2000-01-02", firstSlice.get("end")); + + // 验证最后一个分片 + Map lastSlice = slices.get(slices.size() - 1); + Assertions.assertEquals("2000-01-04", lastSlice.get("start")); + Assertions.assertEquals("2000-01-05", lastSlice.get("end")); + } + + /** + * 测试按天分片并使用不同格式 + */ + @Test + public void testSliceByDayWithDifferentFormats() { + // 准备测试数据 + Date startDate = DateUtil.parse("2000-01-01"); + Date endDate = DateUtil.parse("2000-01-03"); + + // 使用年月日时分秒格式 + List> fullFormatSlices = DateUtil.sliceByDayAndFormat(startDate, endDate, 1, "yyyy-MM-dd HH:mm:ss"); + + // 验证结果 + Assertions.assertEquals(2, fullFormatSlices.size()); + + // 打印结果 + System.out.println("=== 按天分片并格式化测试 (yyyy-MM-dd HH:mm:ss) ==="); + for (Map slice : fullFormatSlices) { + System.out.println("{"); + System.out.println(" \"start\": \"" + slice.get("start") + "\","); + System.out.println(" \"end\": \"" + slice.get("end") + "\""); + System.out.println("}"); + } + + // 验证第一个分片 + Map firstSlice = fullFormatSlices.get(0); + Assertions.assertEquals("2000-01-01 00:00:00", firstSlice.get("start")); + Assertions.assertEquals("2000-01-02 00:00:00", firstSlice.get("end")); + + // 使用自定义格式 + List> customFormatSlices = DateUtil.sliceByDayAndFormat(startDate, endDate, 1, "yyyy年MM月dd日"); + + // 打印结果 + System.out.println("=== 按天分片并格式化测试 (yyyy年MM月dd日) ==="); + for (Map slice : customFormatSlices) { + System.out.println("{"); + System.out.println(" \"start\": \"" + slice.get("start") + "\","); + System.out.println(" \"end\": \"" + slice.get("end") + "\""); + System.out.println("}"); + } + + // 验证第一个分片 + Map customFirstSlice = customFormatSlices.get(0); + Assertions.assertEquals("2000年01月01日", customFirstSlice.get("start")); + Assertions.assertEquals("2000年01月02日", customFirstSlice.get("end")); + } + + /** + * 测试按小时分片并格式化 + */ + @Test + public void testSliceByHourAndFormat() { + // 准备测试数据 + Date startDate = DateUtil.parse("2000-01-01 00:00:00"); + Date endDate = DateUtil.parse("2000-01-01 10:00:00"); + + // 按2小时分片,并使用自定义格式 + List> slices = DateUtil.sliceByHourAndFormat(startDate, endDate, 2, "yyyy-MM-dd HH:mm"); + + // 验证结果 + Assertions.assertEquals(5, slices.size()); + + // 打印结果 + System.out.println("=== 按小时分片并格式化测试 ==="); + for (Map slice : slices) { + System.out.println("{"); + System.out.println(" \"start\": \"" + slice.get("start") + "\","); + System.out.println(" \"end\": \"" + slice.get("end") + "\""); + System.out.println("}"); + } + + // 验证第一个分片 + Map firstSlice = slices.get(0); + Assertions.assertEquals("2000-01-01 00:00", firstSlice.get("start")); + Assertions.assertEquals("2000-01-01 02:00", firstSlice.get("end")); + + // 验证最后一个分片 + Map lastSlice = slices.get(slices.size() - 1); + Assertions.assertEquals("2000-01-01 08:00", lastSlice.get("start")); + Assertions.assertEquals("2000-01-01 10:00", lastSlice.get("end")); + } + + /** + * 测试按分钟分片并格式化 + */ + @Test + public void testSliceByMinuteAndFormat() { + // 准备测试数据 + Date startDate = DateUtil.parse("2000-01-01 00:00:00"); + Date endDate = DateUtil.parse("2000-01-01 00:30:00"); + + // 按5分钟分片,并使用自定义格式 + List> slices = DateUtil.sliceByMinuteAndFormat(startDate, endDate, 5, "HH:mm:ss"); + + // 验证结果 + Assertions.assertEquals(6, slices.size()); + + // 打印结果 + System.out.println("=== 按分钟分片并格式化测试 ==="); + for (Map slice : slices) { + System.out.println("{"); + System.out.println(" \"start\": \"" + slice.get("start") + "\","); + System.out.println(" \"end\": \"" + slice.get("end") + "\""); + System.out.println("}"); + } + + // 验证第一个分片 + Map firstSlice = slices.get(0); + Assertions.assertEquals("00:00:00", firstSlice.get("start")); + Assertions.assertEquals("00:05:00", firstSlice.get("end")); + + // 验证最后一个分片 + Map lastSlice = slices.get(slices.size() - 1); + Assertions.assertEquals("00:25:00", lastSlice.get("start")); + Assertions.assertEquals("00:30:00", lastSlice.get("end")); + } + + /** + * 测试自定义时间单位分片并格式化 + */ + @Test + public void testCustomSliceAndFormat() { + // 准备测试数据 + Date startDate = DateUtil.parse("2000-01-01"); + Date endDate = DateUtil.parse("2000-04-01"); + + // 按1个月分片,并使用自定义格式 + List> slices = DateUtil.sliceAndFormat(startDate, endDate, DateField.MONTH, 1, "yyyy年MM月"); + + // 验证结果 + Assertions.assertEquals(3, slices.size()); + + // 打印结果 + System.out.println("=== 按月分片并格式化测试 ==="); + for (Map slice : slices) { + System.out.println("{"); + System.out.println(" \"start\": \"" + slice.get("start") + "\","); + System.out.println(" \"end\": \"" + slice.get("end") + "\""); + System.out.println("}"); + } + + // 验证第一个分片 + Map firstSlice = slices.get(0); + Assertions.assertEquals("2000年01月", firstSlice.get("start")); + Assertions.assertEquals("2000年02月", firstSlice.get("end")); + + // 验证最后一个分片 + Map lastSlice = slices.get(slices.size() - 1); + Assertions.assertEquals("2000年03月", lastSlice.get("start")); + Assertions.assertEquals("2000年04月", lastSlice.get("end")); + } +} diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/date/DateUtilSliceTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/date/DateUtilSliceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c58c91c67c49cfa90302612c2f5ce122cedd619e --- /dev/null +++ b/hutool-core/src/test/java/org/dromara/hutool/core/date/DateUtilSliceTest.java @@ -0,0 +1,184 @@ +package org.dromara.hutool.core.date; + + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * DateUtil时间分片功能测试用例 + * + * @author Hutool + */ +public class DateUtilSliceTest { + + + /** + * 格式化日期,用于输出 + * + * @param date 日期 + * @return 格式化后的日期字符串 + */ + private String formatDate(Date date) { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); + } + + /** + * 测试按天分片 + */ + @Test + public void testSliceByDay() { + // 准备测试数据 + Date startDate = DateUtil.parse("2000-01-01"); + Date endDate = DateUtil.parse("2000-01-05"); + + // 按1天分片 + List> slices = DateUtil.sliceByDay(startDate, endDate, 1); + + // 验证结果 + Assertions.assertEquals(4, slices.size()); + + // 打印结果 + System.out.println(slices); + + // 验证第一个分片 + Map firstSlice = slices.get(0); + Assertions.assertEquals("2000-01-01 00:00:00", formatDate(firstSlice.get("start"))); + Assertions.assertEquals("2000-01-02 00:00:00", formatDate(firstSlice.get("end"))); + + // 验证最后一个分片 + Map lastSlice = slices.get(slices.size() - 1); + Assertions.assertEquals("2000-01-04 00:00:00", formatDate(lastSlice.get("start"))); + Assertions.assertEquals("2000-01-05 00:00:00", formatDate(lastSlice.get("end"))); + } + + /** + * 测试按小时分片 + */ + @Test + public void testSliceByHour() { + // 准备测试数据 + Date startDate = DateUtil.parse("2000-01-01 00:00:00"); + Date endDate = DateUtil.parse("2000-01-01 10:00:00"); + + // 按2小时分片 + List> slices = DateUtil.sliceByHour(startDate, endDate, 2); + + // 验证结果 + Assertions.assertEquals(5, slices.size()); + + // 打印结果 + System.out.println("=== 按小时分片测试 ==="); + for (Map slice : slices) { + System.out.println("{"); + System.out.println(" \"start\": \"" + formatDate(slice.get("start")) + "\","); + System.out.println(" \"end\": \"" + formatDate(slice.get("end")) + "\""); + System.out.println("}"); + } + + // 验证第一个分片 + Map firstSlice = slices.get(0); + Assertions.assertEquals("2000-01-01 00:00:00", formatDate(firstSlice.get("start"))); + Assertions.assertEquals("2000-01-01 02:00:00", formatDate(firstSlice.get("end"))); + + // 验证最后一个分片 + Map lastSlice = slices.get(slices.size() - 1); + Assertions.assertEquals("2000-01-01 08:00:00", formatDate(lastSlice.get("start"))); + Assertions.assertEquals("2000-01-01 10:00:00", formatDate(lastSlice.get("end"))); + } + + /** + * 测试按分钟分片 + */ + @Test + public void testSliceByMinute() { + // 准备测试数据 + Date startDate = DateUtil.parse("2000-01-01 00:00:00"); + Date endDate = DateUtil.parse("2000-01-01 00:30:00"); + + // 按5分钟分片 + List> slices = DateUtil.sliceByMinute(startDate, endDate, 5); + + // 验证结果 + Assertions.assertEquals(6, slices.size()); + + // 打印结果 + System.out.println("=== 按分钟分片测试 ==="); + for (Map slice : slices) { + System.out.println("{"); + System.out.println(" \"start\": \"" + formatDate(slice.get("start")) + "\","); + System.out.println(" \"end\": \"" + formatDate(slice.get("end")) + "\""); + System.out.println("}"); + } + + // 验证第一个分片 + Map firstSlice = slices.get(0); + Assertions.assertEquals("2000-01-01 00:00:00", formatDate(firstSlice.get("start"))); + Assertions.assertEquals("2000-01-01 00:05:00", formatDate(firstSlice.get("end"))); + + // 验证最后一个分片 + Map lastSlice = slices.get(slices.size() - 1); + Assertions.assertEquals("2000-01-01 00:25:00", formatDate(lastSlice.get("start"))); + Assertions.assertEquals("2000-01-01 00:30:00", formatDate(lastSlice.get("end"))); + } + + /** + * 测试自定义时间单位分片 + */ + @Test + public void testCustomSlice() { + // 准备测试数据 + Date startDate = DateUtil.parse("2000-01-01"); + Date endDate = DateUtil.parse("2000-04-01"); + + // 按1个月分片 + List> slices = DateUtil.slice(startDate, endDate, DateField.MONTH, 1); + + // 验证结果 + Assertions.assertEquals(3, slices.size()); + + // 打印结果 + System.out.println("=== 按月分片测试 ==="); + for (Map slice : slices) { + System.out.println("{"); + System.out.println(" \"start\": \"" + formatDate(slice.get("start")) + "\","); + System.out.println(" \"end\": \"" + formatDate(slice.get("end")) + "\""); + System.out.println("}"); + } + + // 验证第一个分片 + Map firstSlice = slices.get(0); + Assertions.assertEquals("2000-01-01 00:00:00", formatDate(firstSlice.get("start"))); + Assertions.assertEquals("2000-02-01 00:00:00", formatDate(firstSlice.get("end"))); + + // 验证最后一个分片 + Map lastSlice = slices.get(slices.size() - 1); + Assertions.assertEquals("2000-03-01 00:00:00", formatDate(lastSlice.get("start"))); + Assertions.assertEquals("2000-04-01 00:00:00", formatDate(lastSlice.get("end"))); + } + + /** + * 测试边界情况 + */ + @Test + public void testEdgeCases() { + // 测试开始时间等于结束时间 + Date sameDate = DateUtil.parse("2000-01-01"); + List> emptySlices = DateUtil.sliceByDay(sameDate, sameDate, 1); + Assertions.assertTrue(emptySlices.isEmpty()); + + // 测试步长大于总时间间隔 + Date startDate = DateUtil.parse("2000-01-01"); + Date endDate = DateUtil.parse("2000-01-02"); + List> singleSlice = DateUtil.sliceByDay(startDate, endDate, 2); + Assertions.assertEquals(1, singleSlice.size()); + + // 测试参数为null + List> nullSlices = DateUtil.sliceByDay(null, endDate, 1); + Assertions.assertTrue(nullSlices.isEmpty()); + } +}