场景
在统计时间范围的数据时,数据库中有可能缺少一部分数据,比如:
- 按小时统计:数据库中只有 2021-08-01 03 、2021-08-01 08 点的汇总数据
首先这两个时间点中缺少以下小时数据,但是展示曲线图的时候,需要填充好这些数据,使用 0 填充
2021-08-01 04、2021-08-01 05、2021-08-01 06、2021-08-01 07
- 按天统计:数据库中只有 2021-08-01、2021-08-03 的数据,中间缺少了 2021-08-02 的数据,同样需要用 0 填充
以上两个举例,在时间跨度很大的时候,就比较麻烦了,所以有了该工具类
方案
import java.util.ArrayList;import java.util.List;import java.util.Map;import java.util.function.Function;import java.util.function.Supplier;import java.util.stream.Collectors;import cn.hutool.core.date.DateTime;import cn.hutool.core.date.DateUnit;import cn.hutool.core.date.DateUtil;import cn.hutool.core.util.ArrayUtil;import lombok.extern.slf4j.Slf4j;/*** @author mrcode* @date 2021/8/30*/public class StatisticsOverviewService {/*** 填充缺少的 小时 数据,缺少的值一般用 0 填充; 注意:会从第一条数据的 00 小时开始填充,直到最后一条数据 23 点** @param res 至少需要两条数据* @param defaultObjectFactory 默认值对象工厂,当需要填充时,请返回一个用 0 填充的对象* @param <T>* @return*/public static <T extends StatisticsOverviewDataItem> List<T> fillDataOfHour(List<T> res,Supplier<T> defaultObjectFactory) {final T first = res.get(0);final T end = res.get(res.size() - 1);final int firstTime = first.getTime();DateTime firstTimeDate = DateUtil.parse(firstTime + "", "yyyyMMddHH");firstTimeDate = DateUtil.beginOfDay(firstTimeDate); // 一天的开始final int endTime = end.getTime();DateTime endTimeDate = DateUtil.parse(endTime + "", "yyyyMMddHH");endTimeDate = DateUtil.endOfDay(endTimeDate); // 一天的结束final Map<Integer, T> dataMppings = res.parallelStream().collect(Collectors.toMap(T::getTime, Function.identity()));final long betweenHour = DateUtil.between(firstTimeDate, endTimeDate, DateUnit.HOUR);List<T> result = new ArrayList<>((int) betweenHour + 1);DateTime tempStartTime = firstTimeDate;String currentDayStr = ""; // 20210201int currentDayOfHour = 0; // 当前处理的小时while (tempStartTime.isBefore(endTimeDate)) {currentDayStr = DateUtil.format(tempStartTime, "yyyyMMdd");for (int i = 0; i < 24; i++) {currentDayOfHour = Integer.parseInt(currentDayStr + (i < 10 ? "0" + i : i));final T item = dataMppings.get(currentDayOfHour);if (item == null) {final T newItem = defaultObjectFactory.get();newItem.setTime(currentDayOfHour);result.add(newItem);} else {result.add(item);}}tempStartTime = DateUtil.offsetDay(tempStartTime, 1);}return result;}/*** 填充缺少的 天 数据,缺少的值一般用 0 填充;** @param res 至少需要两条数据* @param defaultObjectFactory 默认值对象工厂,当需要填充时,请返回一个用 0 填充的对象* @param <T>* @return*/public static <T extends StatisticsOverviewDataItem> List<T> fillDataOfDay(List<T> res,Supplier<T> defaultObjectFactory) {final T first = res.get(0);final T end = res.get(res.size() - 1);final int firstTime = first.getTime();final DateTime firstTimeDate = DateUtil.beginOfDay(DateUtil.parse(firstTime + "", "yyyyMMdd"));final int endTime = end.getTime();final DateTime endTimeDate = DateUtil.beginOfDay(DateUtil.parse(endTime + "", "yyyyMMdd"));// 计算中间相差多少天,如 20210102 和 20210104,相差大于 1,则表示有不连续的日期,有可能需要填充 20210103 日期的数据final long betweenDay = DateUtil.betweenDay(firstTimeDate, endTimeDate, false);if (betweenDay == 1) {return res;}// 处理中间有可能缺失是数据DateTime tempStartTime = firstTimeDate;final Map<Integer, T> dataMppings = res.parallelStream().collect(Collectors.toMap(T::getTime, Function.identity()));List<T> result = new ArrayList<>((int) betweenDay + 1);int currentDay = 0;while (tempStartTime.isBeforeOrEquals(endTimeDate)) {currentDay = Integer.parseInt(DateUtil.format(tempStartTime, "yyyyMMdd"));final T item = dataMppings.get(currentDay);if (item == null) {final T newItem = defaultObjectFactory.get();newItem.setTime(currentDay);result.add(newItem);} else {result.add(item);}tempStartTime = DateUtil.offsetDay(tempStartTime, 1);}return result;}/*** 将字符串时间范围转换为 int 时间范围** @param timeRange,比如 2021、2021-02、2021-02-01* @return*/public static Integer[] timeRangeToTimeRangeInt(String[] timeRange) {Integer[] result = new Integer[2];if (ArrayUtil.isEmpty(timeRange)) {return result;}final String startItem = timeRange[0];final String endItem = timeRange[1];if (startItem != null) {result[0] = Integer.parseInt(startItem.replace("-", ""));}if (endItem != null) {result[1] = Integer.parseInt(endItem.replace("-", ""));}return result;}}
public interface StatisticsOverviewDataItem {int getTime();void setTime(int time);}
测试类
import org.junit.jupiter.api.Test;import java.util.ArrayList;import java.util.List;/*** @author mrcode* @date 2021/8/30*/class StatisticsOverviewServiceTest {@Testvoid fillDataOfHour() {final ArrayList<DataItem> res = new ArrayList<>();res.add(new DataItem(2021082020, 36));res.add(new DataItem(2021082105, 20));final List<DataItem> dataItems = StatisticsOverviewService.fillDataOfHour(res, DataItem::defaultObject);for (DataItem dataItem : dataItems) {System.out.println(dataItem);}}@Testvoid fillDataOfDay() {final ArrayList<DataItem> res = new ArrayList<>();res.add(new DataItem(20201230, 36));res.add(new DataItem(20210106, 20));final List<DataItem> dataItems = StatisticsOverviewService.fillDataOfDay(res, DataItem::defaultObject);for (DataItem dataItem : dataItems) {System.out.println(dataItem);}}static class DataItem implements StatisticsOverviewDataItem {private int count; // 数量private int time; // 时间public DataItem() {}public DataItem(int time, int count) {this.count = count;this.time = time;}public static DataItem defaultObject() {final DataItem item = new DataItem();item.setCount(0);return item;}@Overridepublic int getTime() {return time;}@Overridepublic void setTime(int time) {this.time = time;}public int getCount() {return count;}public void setCount(int count) {this.count = count;}@Overridepublic String toString() {return "DataItem{" +"time=" + time +", count=" + count +'}';}}}
测试信息如下:
fillDataOfHour 测试
DataItem{time=2021082000, count=0}DataItem{time=2021082001, count=0}DataItem{time=2021082002, count=0}DataItem{time=2021082003, count=0}DataItem{time=2021082004, count=0}DataItem{time=2021082005, count=0}DataItem{time=2021082006, count=0}DataItem{time=2021082007, count=0}DataItem{time=2021082008, count=0}DataItem{time=2021082009, count=0}DataItem{time=2021082010, count=0}DataItem{time=2021082011, count=0}DataItem{time=2021082012, count=0}DataItem{time=2021082013, count=0}DataItem{time=2021082014, count=0}DataItem{time=2021082015, count=0}DataItem{time=2021082016, count=0}DataItem{time=2021082017, count=0}DataItem{time=2021082018, count=0}DataItem{time=2021082019, count=0}DataItem{time=2021082020, count=36}DataItem{time=2021082021, count=0}DataItem{time=2021082022, count=0}DataItem{time=2021082023, count=0}DataItem{time=2021082100, count=0}DataItem{time=2021082101, count=0}DataItem{time=2021082102, count=0}DataItem{time=2021082103, count=0}DataItem{time=2021082104, count=0}DataItem{time=2021082105, count=20}DataItem{time=2021082106, count=0}DataItem{time=2021082107, count=0}DataItem{time=2021082108, count=0}DataItem{time=2021082109, count=0}DataItem{time=2021082110, count=0}DataItem{time=2021082111, count=0}DataItem{time=2021082112, count=0}DataItem{time=2021082113, count=0}DataItem{time=2021082114, count=0}DataItem{time=2021082115, count=0}DataItem{time=2021082116, count=0}DataItem{time=2021082117, count=0}DataItem{time=2021082118, count=0}DataItem{time=2021082119, count=0}DataItem{time=2021082120, count=0}DataItem{time=2021082121, count=0}DataItem{time=2021082122, count=0}DataItem{time=2021082123, count=0}
fillDataOfDay 测试
# 跨年填充也是正常的DataItem{time=20201230, count=36}DataItem{time=20201231, count=0}DataItem{time=20210101, count=0}DataItem{time=20210102, count=0}DataItem{time=20210103, count=0}DataItem{time=20210104, count=0}DataItem{time=20210105, count=0}DataItem{time=20210106, count=20}
