场景

在统计时间范围的数据时,数据库中有可能缺少一部分数据,比如:

  1. 按小时统计:数据库中只有 2021-08-01 03 、2021-08-01 08 点的汇总数据

首先这两个时间点中缺少以下小时数据,但是展示曲线图的时候,需要填充好这些数据,使用 0 填充

  1. 2021-08-01 042021-08-01 052021-08-01 062021-08-01 07
  1. 按天统计:数据库中只有 2021-08-01、2021-08-03 的数据,中间缺少了 2021-08-02 的数据,同样需要用 0 填充

以上两个举例,在时间跨度很大的时候,就比较麻烦了,所以有了该工具类

方案

  1. import java.util.ArrayList;
  2. import java.util.List;
  3. import java.util.Map;
  4. import java.util.function.Function;
  5. import java.util.function.Supplier;
  6. import java.util.stream.Collectors;
  7. import cn.hutool.core.date.DateTime;
  8. import cn.hutool.core.date.DateUnit;
  9. import cn.hutool.core.date.DateUtil;
  10. import cn.hutool.core.util.ArrayUtil;
  11. import lombok.extern.slf4j.Slf4j;
  12. /**
  13. * @author mrcode
  14. * @date 2021/8/30
  15. */
  16. public class StatisticsOverviewService {
  17. /**
  18. * 填充缺少的 小时 数据,缺少的值一般用 0 填充; 注意:会从第一条数据的 00 小时开始填充,直到最后一条数据 23 点
  19. *
  20. * @param res 至少需要两条数据
  21. * @param defaultObjectFactory 默认值对象工厂,当需要填充时,请返回一个用 0 填充的对象
  22. * @param <T>
  23. * @return
  24. */
  25. public static <T extends StatisticsOverviewDataItem> List<T> fillDataOfHour(List<T> res,
  26. Supplier<T> defaultObjectFactory) {
  27. final T first = res.get(0);
  28. final T end = res.get(res.size() - 1);
  29. final int firstTime = first.getTime();
  30. DateTime firstTimeDate = DateUtil.parse(firstTime + "", "yyyyMMddHH");
  31. firstTimeDate = DateUtil.beginOfDay(firstTimeDate); // 一天的开始
  32. final int endTime = end.getTime();
  33. DateTime endTimeDate = DateUtil.parse(endTime + "", "yyyyMMddHH");
  34. endTimeDate = DateUtil.endOfDay(endTimeDate); // 一天的结束
  35. final Map<Integer, T> dataMppings = res.parallelStream().collect(Collectors.toMap(T::getTime, Function.identity()));
  36. final long betweenHour = DateUtil.between(firstTimeDate, endTimeDate, DateUnit.HOUR);
  37. List<T> result = new ArrayList<>((int) betweenHour + 1);
  38. DateTime tempStartTime = firstTimeDate;
  39. String currentDayStr = ""; // 20210201
  40. int currentDayOfHour = 0; // 当前处理的小时
  41. while (tempStartTime.isBefore(endTimeDate)) {
  42. currentDayStr = DateUtil.format(tempStartTime, "yyyyMMdd");
  43. for (int i = 0; i < 24; i++) {
  44. currentDayOfHour = Integer.parseInt(currentDayStr + (i < 10 ? "0" + i : i));
  45. final T item = dataMppings.get(currentDayOfHour);
  46. if (item == null) {
  47. final T newItem = defaultObjectFactory.get();
  48. newItem.setTime(currentDayOfHour);
  49. result.add(newItem);
  50. } else {
  51. result.add(item);
  52. }
  53. }
  54. tempStartTime = DateUtil.offsetDay(tempStartTime, 1);
  55. }
  56. return result;
  57. }
  58. /**
  59. * 填充缺少的 天 数据,缺少的值一般用 0 填充;
  60. *
  61. * @param res 至少需要两条数据
  62. * @param defaultObjectFactory 默认值对象工厂,当需要填充时,请返回一个用 0 填充的对象
  63. * @param <T>
  64. * @return
  65. */
  66. public static <T extends StatisticsOverviewDataItem> List<T> fillDataOfDay(List<T> res,
  67. Supplier<T> defaultObjectFactory) {
  68. final T first = res.get(0);
  69. final T end = res.get(res.size() - 1);
  70. final int firstTime = first.getTime();
  71. final DateTime firstTimeDate = DateUtil.beginOfDay(DateUtil.parse(firstTime + "", "yyyyMMdd"));
  72. final int endTime = end.getTime();
  73. final DateTime endTimeDate = DateUtil.beginOfDay(DateUtil.parse(endTime + "", "yyyyMMdd"));
  74. // 计算中间相差多少天,如 20210102 和 20210104,相差大于 1,则表示有不连续的日期,有可能需要填充 20210103 日期的数据
  75. final long betweenDay = DateUtil.betweenDay(firstTimeDate, endTimeDate, false);
  76. if (betweenDay == 1) {
  77. return res;
  78. }
  79. // 处理中间有可能缺失是数据
  80. DateTime tempStartTime = firstTimeDate;
  81. final Map<Integer, T> dataMppings = res.parallelStream().collect(Collectors.toMap(T::getTime, Function.identity()));
  82. List<T> result = new ArrayList<>((int) betweenDay + 1);
  83. int currentDay = 0;
  84. while (tempStartTime.isBeforeOrEquals(endTimeDate)) {
  85. currentDay = Integer.parseInt(DateUtil.format(tempStartTime, "yyyyMMdd"));
  86. final T item = dataMppings.get(currentDay);
  87. if (item == null) {
  88. final T newItem = defaultObjectFactory.get();
  89. newItem.setTime(currentDay);
  90. result.add(newItem);
  91. } else {
  92. result.add(item);
  93. }
  94. tempStartTime = DateUtil.offsetDay(tempStartTime, 1);
  95. }
  96. return result;
  97. }
  98. /**
  99. * 将字符串时间范围转换为 int 时间范围
  100. *
  101. * @param timeRange,比如 2021、2021-02、2021-02-01
  102. * @return
  103. */
  104. public static Integer[] timeRangeToTimeRangeInt(String[] timeRange) {
  105. Integer[] result = new Integer[2];
  106. if (ArrayUtil.isEmpty(timeRange)) {
  107. return result;
  108. }
  109. final String startItem = timeRange[0];
  110. final String endItem = timeRange[1];
  111. if (startItem != null) {
  112. result[0] = Integer.parseInt(startItem.replace("-", ""));
  113. }
  114. if (endItem != null) {
  115. result[1] = Integer.parseInt(endItem.replace("-", ""));
  116. }
  117. return result;
  118. }
  119. }
  1. public interface StatisticsOverviewDataItem {
  2. int getTime();
  3. void setTime(int time);
  4. }

测试类

  1. import org.junit.jupiter.api.Test;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. /**
  5. * @author mrcode
  6. * @date 2021/8/30
  7. */
  8. class StatisticsOverviewServiceTest {
  9. @Test
  10. void fillDataOfHour() {
  11. final ArrayList<DataItem> res = new ArrayList<>();
  12. res.add(new DataItem(2021082020, 36));
  13. res.add(new DataItem(2021082105, 20));
  14. final List<DataItem> dataItems = StatisticsOverviewService.fillDataOfHour(res, DataItem::defaultObject);
  15. for (DataItem dataItem : dataItems) {
  16. System.out.println(dataItem);
  17. }
  18. }
  19. @Test
  20. void fillDataOfDay() {
  21. final ArrayList<DataItem> res = new ArrayList<>();
  22. res.add(new DataItem(20201230, 36));
  23. res.add(new DataItem(20210106, 20));
  24. final List<DataItem> dataItems = StatisticsOverviewService.fillDataOfDay(res, DataItem::defaultObject);
  25. for (DataItem dataItem : dataItems) {
  26. System.out.println(dataItem);
  27. }
  28. }
  29. static class DataItem implements StatisticsOverviewDataItem {
  30. private int count; // 数量
  31. private int time; // 时间
  32. public DataItem() {
  33. }
  34. public DataItem(int time, int count) {
  35. this.count = count;
  36. this.time = time;
  37. }
  38. public static DataItem defaultObject() {
  39. final DataItem item = new DataItem();
  40. item.setCount(0);
  41. return item;
  42. }
  43. @Override
  44. public int getTime() {
  45. return time;
  46. }
  47. @Override
  48. public void setTime(int time) {
  49. this.time = time;
  50. }
  51. public int getCount() {
  52. return count;
  53. }
  54. public void setCount(int count) {
  55. this.count = count;
  56. }
  57. @Override
  58. public String toString() {
  59. return "DataItem{" +
  60. "time=" + time +
  61. ", count=" + count +
  62. '}';
  63. }
  64. }
  65. }

测试信息如下:
fillDataOfHour 测试

  1. DataItem{time=2021082000, count=0}
  2. DataItem{time=2021082001, count=0}
  3. DataItem{time=2021082002, count=0}
  4. DataItem{time=2021082003, count=0}
  5. DataItem{time=2021082004, count=0}
  6. DataItem{time=2021082005, count=0}
  7. DataItem{time=2021082006, count=0}
  8. DataItem{time=2021082007, count=0}
  9. DataItem{time=2021082008, count=0}
  10. DataItem{time=2021082009, count=0}
  11. DataItem{time=2021082010, count=0}
  12. DataItem{time=2021082011, count=0}
  13. DataItem{time=2021082012, count=0}
  14. DataItem{time=2021082013, count=0}
  15. DataItem{time=2021082014, count=0}
  16. DataItem{time=2021082015, count=0}
  17. DataItem{time=2021082016, count=0}
  18. DataItem{time=2021082017, count=0}
  19. DataItem{time=2021082018, count=0}
  20. DataItem{time=2021082019, count=0}
  21. DataItem{time=2021082020, count=36}
  22. DataItem{time=2021082021, count=0}
  23. DataItem{time=2021082022, count=0}
  24. DataItem{time=2021082023, count=0}
  25. DataItem{time=2021082100, count=0}
  26. DataItem{time=2021082101, count=0}
  27. DataItem{time=2021082102, count=0}
  28. DataItem{time=2021082103, count=0}
  29. DataItem{time=2021082104, count=0}
  30. DataItem{time=2021082105, count=20}
  31. DataItem{time=2021082106, count=0}
  32. DataItem{time=2021082107, count=0}
  33. DataItem{time=2021082108, count=0}
  34. DataItem{time=2021082109, count=0}
  35. DataItem{time=2021082110, count=0}
  36. DataItem{time=2021082111, count=0}
  37. DataItem{time=2021082112, count=0}
  38. DataItem{time=2021082113, count=0}
  39. DataItem{time=2021082114, count=0}
  40. DataItem{time=2021082115, count=0}
  41. DataItem{time=2021082116, count=0}
  42. DataItem{time=2021082117, count=0}
  43. DataItem{time=2021082118, count=0}
  44. DataItem{time=2021082119, count=0}
  45. DataItem{time=2021082120, count=0}
  46. DataItem{time=2021082121, count=0}
  47. DataItem{time=2021082122, count=0}
  48. DataItem{time=2021082123, count=0}

fillDataOfDay 测试

  1. # 跨年填充也是正常的
  2. DataItem{time=20201230, count=36}
  3. DataItem{time=20201231, count=0}
  4. DataItem{time=20210101, count=0}
  5. DataItem{time=20210102, count=0}
  6. DataItem{time=20210103, count=0}
  7. DataItem{time=20210104, count=0}
  8. DataItem{time=20210105, count=0}
  9. DataItem{time=20210106, count=20}