報表導出、定時任務分片、數據歸檔常需將一個大時間範圍 [start, end] 切分為等長的小區間:
public final class DateRangeSplitter {
private DateRangeSplitter() {}
public enum Unit { MONTH, DAY, HOUR, MINUTE }
/**
* 切分區間,返回所有「分割點」時間戳(包含 start,不包含 end 末尾點)
* 例如 DAY: 2023-05-01 00:00:00, 2023-05-02 00:00:00 ... 2023-05-10 00:00:00
*/
public static List split(LocalDateTime start, LocalDateTime end, Unit unit) {
if (start.isAfter(end)) throw new IllegalArgumentException("start 必須 <= end");
List points = new ArrayList<>();
LocalDateTime cur = start;
points.add(cur);
while (cur.isBefore(end)) {
cur = switch (unit) {
case MONTH -> cur.plusMonths(1).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
case DAY -> cur.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
case HOUR -> cur.plusHours(1).withMinute(0).withSecond(0).withNano(0);
case MINUTE -> cur.plusMinutes(1).withSecond(0).withNano(0);
};
if (cur.isBefore(end) || cur.isEqual(end)) points.add(cur);
}
return points;
}
/** 返回區間對象列表,每個區間為 [start, end) 左閉右開 */
public static List splitToRanges(LocalDateTime start, LocalDateTime end, Unit unit) {
List points = split(start, end, unit);
List ranges = new ArrayList<>();
for (int i = 0; i < points.size() - 1; i++) {
ranges.add(new Range(points.get(i), points.get(i + 1)));
}
return ranges;
}
public record Range(LocalDateTime startInclusive, LocalDateTime endExclusive) {}
}
public class Demo {
public static void main(String[] args) {
LocalDateTime start = LocalDateTime.of(2023, 2, 1, 0, 0);
LocalDateTime end = LocalDateTime.of(2023, 3, 2, 0, 0);
// 按天切分
List days = DateRangeSplitter.splitToRanges(start, end, DateRangeSplitter.Unit.DAY);
days.forEach(r -> System.out.println(r.startInclusive() + " ~ " + r.endExclusive()));
// 輸出 2023-02-01T00:00 ~ 2023-02-02T00:00 ... 2023-03-01T00:00 ~ 2023-03-02T00:00
// 按小時切分(跨天自動處理)
List hours = DateRangeSplitter.splitToRanges(
LocalDateTime.of(2023, 5, 1, 10, 30),
LocalDateTime.of(2023, 5, 1, 14, 45),
DateRangeSplitter.Unit.HOUR);
hours.forEach(System.out::println);
// 2023-05-01T10:30 ~ 2023-05-01T11:00
// 2023-05-01T11:00 ~ 2023-05-01T12:00
// 2023-05-01T12:00 ~ 2023-05-01T13:00
// 2023-05-01T13:00 ~ 2023-05-01T14:00
// 2023-05-01T14:00 ~ 2023-05-01T14:45 (最後一段不足 1 小時)
}
}
public static List splitLegacy(Date start, Date end, int calendarField) {
List points = new ArrayList<>();
Calendar cal = Calendar.getInstance();
cal.setTime(start);
// 對齊到字段起始
alignToFieldStart(cal, calendarField);
points.add(cal.getTime());
while (cal.getTime().before(end)) {
cal.add(calendarField, 1);
if (!cal.getTime().after(end)) points.add(cal.getTime());
}
return points;
}
private static void alignToFieldStart(Calendar cal, int field) {
switch (field) {
case Calendar.MONTH -> { cal.set(Calendar.DAY_OF_MONTH, 1); }
case Calendar.DAY_OF_MONTH, Calendar.DAY_OF_YEAR -> {}
case Calendar.HOUR_OF_DAY -> { cal.set(Calendar.MINUTE, 0); }
case Calendar.MINUTE -> { cal.set(Calendar.SECOND, 0); }
}
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
}
[start, end),避免邊界重疊/遺漏,適合 SQL WHERE ts >= ? AND ts < ?。ZoneId(如 UTC),跨夏令時切分時 java.time 自動處理。將 DateRangeSplitter 放入公共工具包,即可用一行代碼解決所有「大範圍切小片」需求,告別手寫 while (cal.before(end)) cal.add(...) 的重複造輪子。