Java 实现折线图整点数据补全与标准化处理示例代码讲解

一、功能概述

将从第三方接口获取的储罐(温度 / 高度)原始数据,处理成按整点标准化的折线图数据格式,

解决原始数据时间不规整、存在缺失值、重复数据等问题,最终返回连续的整点数据序列给前端,

确保折线图展示连续、无断点。

注:

博客:
https://blog.csdn.net/badao_liumang_qizhi

二、核心功能拆解与代码详解

2.1 数据预处理阶段

复制代码
List<TankQueryResponse.TankInfo> items = result.getData().getItems();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

list = items.stream()
        // 过滤掉更新时间为空的数据(防御性处理)
        .filter(tankInfo -> tankInfo.getUpdatedAt() !=null)
        .map(source -> {
            try {
                WarehouseTankThirdLogNewDTO tankThirdLog = new WarehouseTankThirdLogNewDTO();
                // 字符串转BigDecimal(温度/高度)
                tankThirdLog.setTemperature(parseBigDecimal(source.getTemperature()));
                tankThirdLog.setHeight(parseBigDecimal(source.getHeight()));
                // 时间字符串转Date类型
                Date date = format.parse(source.getUpdatedAt());
                tankThirdLog.setCreateTime(date);
                return tankThirdLog;
            } catch (Exception e) {
                // 转换异常则返回null,后续过滤
                return null;
            }
        })
        // 过滤掉转换异常的null数据
        .filter(Objects::nonNull)
        .collect(Collectors.toList());

// 按时间排序,为后续处理做准备
list.sort(Comparator.comparing(WarehouseTankThirdLogNewDTO::getCreateTime));

功能说明:

过滤空时间数据:避免后续时间转换出现 NPE(空指针异常);

数据类型转换:将原始字符串类型的温度、高度转为 BigDecimal,时间字符串转为 Date;

异常处理:捕获转换异常并标记为 null,最终过滤掉异常数据;

时间排序:保证数据按时间顺序排列,为后续整点匹配打下基础。

2.2 整点时间序列生成

复制代码
// 确定时间范围(取最早/最晚数据的整点)
if (list.isEmpty()) {
    return Collections.emptyList();
}
List<Date> hourPoints = generateHourPoints(
        roundToHour(list.get(0).getCreateTime(), false),
        roundToHour(list.get(list.size()-1).getCreateTime(), true)
);

核心方法说明:

roundToHour(Date date, boolean ceiling):时间取整到整点,ceiling=false向下取整(如 10:23→10:00),

ceiling=true向上取整(如 10:23→11:00);

generateHourPoints(Date start, Date end):生成从 start 到 end 的所有整点时间列表(如 10:00、11:00、12:00)。

2.3 数据去重与整点归并

复制代码
// 分钟级精度去重:同一分钟内只保留最新数据
Map<Date, WarehouseTankThirdLogNewDTO> minutePrecisionMap = new HashMap<>();
list.forEach(item -> {
    Date minuteKey = roundToMinutePrecision(item.getCreateTime());
    minutePrecisionMap.merge(minuteKey, item,
            (oldVal, newVal) -> oldVal.getCreateTime().after(newVal.getCreateTime()) ? oldVal : newVal);
});

// 严格整点归并:每个整点保留离该整点最近的数据
Map<Date, WarehouseTankThirdLogNewDTO> hourPointMap = new TreeMap<>();
minutePrecisionMap.values().forEach(item -> {
    Date hourKey = roundToStrictHour(item.getCreateTime(), false);
    hourPointMap.merge(hourKey, item, (oldVal, newVal) ->
            Math.abs(oldVal.getCreateTime().getTime() - hourKey.getTime()) <
                    Math.abs(newVal.getCreateTime().getTime() - hourKey.getTime())
                    ? oldVal : newVal
    );
});

核心方法说明:

roundToMinutePrecision(Date date):时间取整到分钟(秒 / 毫秒置 0,如 10:23:45→10:23:00),

解决同一分钟内多条重复数据问题;

roundToStrictHour(Date date, boolean isEndOfHour):严格整点处理(如 23:59→23:59:59,避免跨天问题);

归并逻辑:同一整点下,对比数据时间与整点的差值,保留最近的一条数据。

2.4 缺失整点数据智能补全

复制代码
// 智能填充(带有效性检查)
for (Date hourPoint : hourPoints) {
    if (!hourPointMap.containsKey(hourPoint)) {
        // 查找最近3小时内的有效数据(避免跨日污染)
        WarehouseTankThirdLogNewDTO nearest = findNearestValidWithinRange(
                hourPointMap, hourPoint, 3 * 60 * 60 * 1000);
        if (nearest != null && isValidData(nearest)) {
            hourPointMap.put(hourPoint, copyWithNewTime(nearest, hourPoint));
        }
    }
}

核心方法说明:

isValidData(WarehouseTankThirdLogNewDTO dto):校验数据有效性(温度 / 高度非空);

findNearestValidWithinRange(...):在目标整点 ±3 小时范围内,查找最近的有效数据;

copyWithNewTime(...):复制有效数据,仅替换时间为目标整点(保证数据值不变,时间标准化)。

三、数据流转示例

3.1 原始输入数据

假设从接口获取的原始items数据如下(简化格式):

3.2 预处理后数据

过滤重复、转换类型并排序后:

3.3 整点时间序列生成

最早时间 10:15:23 向下取整→10:00,最晚时间 13:05:30 向上取整→14:00,生成整点列表:

3.4 整点归并结果

3.5 缺失数据补全

12:00 整点无数据,查找 ±3 小时内的有效数据(11:00/13:00),取最近的 11:40:10(26.1,12.5),复制数据并将时间改为 12:00:

3.6 最终返回数据

按时间排序后返回:

四、完整代码

复制代码
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 储罐折线图整点数据处理工具类
 * 功能:标准化原始数据为整点序列,处理缺失/重复数据,保证前端折线图连续展示
 */
public class TankChartDataProcessor {

    // 核心处理方法(简化外层逻辑,保留核心)
    public List<WarehouseTankThirdLogNewDTO> processTankChartData(TankQueryResponse result) {
        List<WarehouseTankThirdLogNewDTO> list = new ArrayList<>();
        if (result != null && result.getData() != null && result.getData().getItems() != null) {
            List<TankQueryResponse.TankInfo> items = result.getData().getItems();
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

            list = items.stream()
                    // 过滤空时间数据(防御性处理)
                    .filter(tankInfo -> tankInfo.getUpdatedAt() != null)
                    .map(source -> {
                        try {
                            WarehouseTankThirdLogNewDTO tankThirdLog = new WarehouseTankThirdLogNewDTO();
                            tankThirdLog.setTemperature(parseBigDecimal(source.getTemperature()));
                            tankThirdLog.setHeight(parseBigDecimal(source.getHeight()));
                            // 字符串转换Date
                            Date date = format.parse(source.getUpdatedAt());
                            tankThirdLog.setCreateTime(date);
                            return tankThirdLog;
                        } catch (Exception e) {
                            // 转换异常标记为null
                            return null;
                        }
                    })
                    // 过滤掉异常null数据
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());

            // 按时间排序
            list.sort(Comparator.comparing(WarehouseTankThirdLogNewDTO::getCreateTime));
            // 确定时间范围
            if (list.isEmpty()) {
                return Collections.emptyList();
            }

            // 生成整点时间序列
            List<Date> hourPoints = generateHourPoints(
                    roundToHour(list.get(0).getCreateTime(), false),
                    roundToHour(list.get(list.size() - 1).getCreateTime(), true)
            );

            // 为每个整点找到最近的数据
            // 构建时间到数据的映射(解决重复问题)
            Map<Date, WarehouseTankThirdLogNewDTO> hourPointMap = new TreeMap<>();
            Map<Date, WarehouseTankThirdLogNewDTO> minutePrecisionMap = new HashMap<>();

            // 分钟级精度去重
            list.forEach(item -> {
                Date minuteKey = roundToMinutePrecision(item.getCreateTime());
                minutePrecisionMap.merge(minuteKey, item,
                        (oldVal, newVal) -> oldVal.getCreateTime().after(newVal.getCreateTime()) ? oldVal : newVal);
            });

            // 严格整点归并
            minutePrecisionMap.values().forEach(item -> {
                Date hourKey = roundToStrictHour(item.getCreateTime(), false);
                hourPointMap.merge(hourKey, item, (oldVal, newVal) ->
                        Math.abs(oldVal.getCreateTime().getTime() - hourKey.getTime()) <
                                Math.abs(newVal.getCreateTime().getTime() - hourKey.getTime())
                                ? oldVal : newVal
                );
            });

            // 智能填充(带有效性检查)
            for (Date hourPoint : hourPoints) {
                if (!hourPointMap.containsKey(hourPoint)) {
                    // 查找最近3小时内的有效数据(避免跨日污染)
                    WarehouseTankThirdLogNewDTO nearest = findNearestValidWithinRange(
                            hourPointMap, hourPoint, 3 * 60 * 60 * 1000);
                    if (nearest != null && isValidData(nearest)) {
                        hourPointMap.put(hourPoint, copyWithNewTime(nearest, hourPoint));
                    }
                }
            }
            // 按时间排序后返回
            return new ArrayList<>(hourPointMap.values());
        }
        // 查不到数据返回空列表
        return Collections.emptyList();
    }

    // 严格小时取整(解决23:59→00:00问题)
    private Date roundToStrictHour(Date date, boolean isEndOfHour) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.set(Calendar.MINUTE, isEndOfHour ? 59 : 0);
        cal.set(Calendar.SECOND, isEndOfHour ? 59 : 0);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTime();
    }

    // 分钟级精度处理
    private Date roundToMinutePrecision(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTime();
    }

    // 有效性检查
    private boolean isValidData(WarehouseTankThirdLogNewDTO dto) {
        return dto.getTemperature() != null && dto.getHeight() != null;
    }

    // 范围内查找有效数据
    private WarehouseTankThirdLogNewDTO findNearestValidWithinRange(
            Map<Date, WarehouseTankThirdLogNewDTO> map, Date target, long rangeMs) {

        return map.entrySet().stream()
                .filter(e -> Math.abs(e.getKey().getTime() - target.getTime()) <= rangeMs)
                .min(Comparator.comparingLong(e ->
                        Math.abs(e.getKey().getTime() - target.getTime())))
                .map(Map.Entry::getValue)
                .orElse(null);
    }

    /**
     * 时间取整(解决边界问题)
     * @param date 原始时间
     * @param ceiling 是否向上取整(false为向下取整)
     */
    private Date roundToHour(Date date, boolean ceiling) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        if (ceiling && (date.getTime() > calendar.getTimeInMillis())) {
            calendar.add(Calendar.HOUR_OF_DAY, 1);
        }
        return calendar.getTime();
    }

    /**
     * 生成从start到end的所有整点时间
     */
    private List<Date> generateHourPoints(Date start, Date end) {
        List<Date> hourPoints = new ArrayList<>();

        Calendar calendar = Calendar.getInstance();
        calendar.setTime(start);
        // 设置为最近的整点
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);

        Calendar endCalendar = Calendar.getInstance();
        endCalendar.setTime(end);
        endCalendar.set(Calendar.MINUTE, 0);
        endCalendar.set(Calendar.SECOND, 0);
        endCalendar.set(Calendar.MILLISECOND, 0);

        while (!calendar.after(endCalendar)) {
            hourPoints.add(calendar.getTime());
            calendar.add(Calendar.HOUR_OF_DAY, 1);
        }

        return hourPoints;
    }

    /**
     * 复制数据对象并设置新的时间
     */
    private WarehouseTankThirdLogNewDTO copyWithNewTime(
            WarehouseTankThirdLogNewDTO source,
            Date newTime) {

        WarehouseTankThirdLogNewDTO copy = new WarehouseTankThirdLogNewDTO();
        copy.setTemperature(source.getTemperature());
        copy.setHeight(source.getHeight());
        copy.setCreateTime(newTime);
        return copy;
    }

    private static BigDecimal parseBigDecimal(String value) {
        return (value != null && !value.isEmpty()) ?
                new BigDecimal(value) :
                new BigDecimal("0");
    }

    /**
     * 备用补点方法:遍历时间范围补充缺失整点
     */
    public static List<WarehouseTankThirdLogNewDTO> fillMissingRecords(LocalDateTime startTime, LocalDateTime endTime, List<WarehouseTankThirdLogNewDTO> recordList) {
        // 遍历从开始时间到结束时间的每个小时
        for (LocalDateTime time = startTime.truncatedTo(ChronoUnit.HOURS); time.isBefore(endTime); time = time.plusHours(1)) {
            // 检查记录中是否有该小时的记录
            boolean exists = false;
            for (WarehouseTankThirdLogNewDTO record : recordList) {
                if (record.getCreateTime().toInstant()
                        .atZone(ZoneId.systemDefault())
                        .toLocalDateTime().truncatedTo(ChronoUnit.HOURS).equals(time)) {
                    exists = true;
                    break;
                }
            }
            // 如果没有该记录,补充一个
            if (!exists) {
                WarehouseTankThirdLogNewDTO record = new WarehouseTankThirdLogNewDTO();
                record.setCreateTime(Date.from(time.atZone(ZoneId.systemDefault()).toInstant()));
                recordList.add(record);
            }
        }
        recordList.sort((r1, r2) -> r1.getCreateTime().compareTo(r2.getCreateTime()));
        return recordList;
    }

    // 内部DTO类(简化定义,仅保留核心字段)
    public static class WarehouseTankThirdLogNewDTO {
        private BigDecimal temperature;
        private BigDecimal height;
        private Date createTime;

        // Getter & Setter
        public BigDecimal getTemperature() {
            return temperature;
        }

        public void setTemperature(BigDecimal temperature) {
            this.temperature = temperature;
        }

        public BigDecimal getHeight() {
            return height;
        }

        public void setHeight(BigDecimal height) {
            this.height = height;
        }

        public Date getCreateTime() {
            return createTime;
        }

        public void setCreateTime(Date createTime) {
            this.createTime = createTime;
        }
    }

    // 模拟第三方返回的响应类
    public static class TankQueryResponse {
        private TankData data;

        public TankData getData() {
            return data;
        }

        public void setData(TankData data) {
            this.data = data;
        }

        public static class TankData {
            private List<TankInfo> items;

            public List<TankInfo> getItems() {
                return items;
            }

            public void setItems(List<TankInfo> items) {
                this.items = items;
            }
        }

        public static class TankInfo {
            private String updatedAt;
            private String temperature;
            private String height;

            public String getUpdatedAt() {
                return updatedAt;
            }

            public void setUpdatedAt(String updatedAt) {
                this.updatedAt = updatedAt;
            }

            public String getTemperature() {
                return temperature;
            }

            public void setTemperature(String temperature) {
                this.temperature = temperature;
            }

            public String getHeight() {
                return height;
            }

            public void setHeight(String height) {
                this.height = height;
            }
        }
    }
}
相关推荐
qq_338032922 小时前
Vue/JS项目的package.json文件 和java项目里面的pom文件
java·javascript·vue.js·json
冬奇Lab2 小时前
【Kotlin系列10】协程原理与实战(上):结构化并发让异步编程不再是噩梦
android·开发语言·kotlin
薛不痒2 小时前
项目:矿物分类(训练模型)
开发语言·人工智能·python·学习·算法·机器学习·分类
jason.zeng@15022072 小时前
spring boot mqtt开发-原生 Paho 手动封装(最高灵活性,完全自定义)
java·spring boot·后端
姜太小白2 小时前
【前端】JavaScript字符串执行方法总结
开发语言·前端·javascript
被星1砸昏头2 小时前
C++与Node.js集成
开发语言·c++·算法
xixi09242 小时前
selenium的安装配置
开发语言·python
sunnyday04262 小时前
Filter、Interceptor、Spring AOP 的执行顺序详解
java·spring boot·后端·spring
程序员zgh2 小时前
C++ 纯虚函数 — 抽象接口
c语言·开发语言·c++·经验分享·笔记·接口隔离原则