场景
在统计时间范围的数据时,数据库中有可能缺少一部分数据,比如:
- 按小时统计:数据库中只有 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 = ""; // 20210201
int 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 {
@Test
void 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);
}
}
@Test
void 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;
}
@Override
public int getTime() {
return time;
}
@Override
public void setTime(int time) {
this.time = time;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
@Override
public 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}