JS数组映射时指定日期范围多维度聚合统计结果错误
问题根因
代码存在三个核心问题导致统计结果不符合预期:
- 聚合唯一键缺失
label维度:生成分组key时仅拼接了日期、studyId、siteId,没有加入状态标签字段,导致同一站点、同一研究下不同状态的条目被错误合并计数。 - 时间匹配逻辑不符合按天统计需求:当前逻辑仅判断单个时间点是否落在状态有效区间内,没有按自然日范围做区间重叠判断,无法正确覆盖全天内生效的所有状态。
- 存储结构不规范:用数组类型存储聚合键值对,虽然JS语法允许,但容易出现原型链属性冲突等隐患,应该使用普通对象存储分组结果。
修正方案
- 对每个批次日期,先计算对应统计自然日的UTC起止时间(当日0点到次日0点)
- 调整区间匹配逻辑,判断状态有效区间和统计日区间是否存在重叠,正确过滤当日有效条目
- 聚合key中加入
label字段,保证分组维度和需求一致(日期+studyId+siteId+label) - 使用普通对象存储聚合结果,避免数组存键值对的隐患
修正后完整代码
const mockData = [ // INCOMPLETE { id: "C1-TBX4", studyId: "TBX4", siteId: null, statusType: "INCOMPLETE", statusFrom: "2020-12-01T12:15:00.000Z", statusTo: "2020-12-05T01:00:00.000Z" }, { id: "C2-TBX4", studyId: "TBX4", siteId: "USA-1", statusType: "INCOMPLETE", statusFrom: "2020-12-01T01:00:00.000Z", statusTo: null }, { id: "C3-TBX4", studyId: "TBX4", siteId: "USA-1", statusType: "INCOMPLETE", statusFrom: "2020-12-01T01:00:00.000Z", statusTo: null }, { id: "C4-TBX4", studyId: "TBX4", siteId: null, statusType: "INCOMPLETE", statusFrom: "2020-12-01T12:15:00.000Z", statusTo: "2020-12-05T01:00:00.000Z" }, // DROPOUT_PRESCREENER { id: "C5-TBX4", studyId: "TBX4", siteId: "USA-1", statusType: "DROPOUT_PRESCREENER", eligible: false, statusFrom: "2020-12-01T01:00:00.000Z", statusTo: null }, // REJECTED_PRESCREENER { id: "C6-TBX4", studyId: "TBX4", siteId: "USA-1", statusType: "REJECTED_PRESCREENER", statusFrom: "2020-12-01T01:00:00.000Z", statusTo: null }, // Out of range { id: "C7-TBX4", studyId: "TBX4", siteId: "USA-1", statusType: "REJECTED_PRESCREENER", statusFrom: "2020-12-02T01:00:00.000Z", statusTo: null } ]; const assignLabel = (statusType, eligible) => { if (statusType === "INCOMPLETE") return "pending"; if (statusType === "REJECTED_PRESCREENER") return "ineligible"; if (statusType === "DROPOUT_PRESCREENER" && eligible) return "eligible"; if (statusType === "DROPOUT_PRESCREENER" && !eligible) return "abandoned"; return "completed"; }; // 传入要统计的自然日0点即可,和日期生成函数逻辑对齐,本示例统计2020-12-01数据 const batches = [new Date("2020-12-01T00:00:00.000Z")]; const screeningNumbersCalculations = () => { const results = {}; batches.forEach((batchStart) => { // 计算当前统计日的结束时间(次日0点) const batchEnd = new Date(batchStart.getTime() + 24 * 60 * 60 * 1000); const dateStr = batchStart.toISOString().split("T")[0]; // 过滤出和当前统计日区间有重叠的状态条目 const matchingStatuses = mockData.filter((s) => { const from = new Date(s.statusFrom); const to = s.statusTo ? new Date(s.statusTo) : Infinity; // 区间重叠判断:状态开始时间早于统计日结束,统计日开始时间早于状态结束时间 return from < batchEnd && batchStart < to; }); matchingStatuses.forEach((ms) => { const label = assignLabel(ms.statusType, ms.eligible); // 聚合key加入label字段,保证维度完整 const key = `${dateStr}${ms.studyId}${ms.siteId}${label}`; if (results[key]) { results[key].total++; } else { results[key] = { days: [dateStr], studyId: ms.studyId, siteId: ms.siteId, screeningStage: "SELF_ASSESSMENT", label: label, total: 1 }; } }); }); return Object.values(results); }; const screeningNumbers = screeningNumbersCalculations(); console.log(JSON.stringify(screeningNumbers, null, 2));
运行结果
执行后输出和预期完全一致:
[ { "days": ["2020-12-01"], "studyId": "TBX4", "siteId": null, "screeningStage": "SELF_ASSESSMENT", "label": "pending", "total": 2 }, { "days": ["2020-12-01"], "studyId": "TBX4", "siteId": "USA-1", "screeningStage": "SELF_ASSESSMENT", "label": "pending", "total": 2 }, { "days": ["2020-12-01"], "studyId": "TBX4", "siteId": "USA-1", "screeningStage": "SELF_ASSESSMENT", "label": "abandoned", "total": 1 }, { "days": ["2020-12-01"], "studyId": "TBX4", "siteId": "USA-1", "screeningStage": "SELF_ASSESSMENT", "label": "ineligible", "total": 1 } ]
注:如果你的批处理逻辑是传入执行日0点统计前一天数据,只需要在计算
batchStart时减去1天的毫秒数即可,不需要改动其他逻辑。
内容的提问来源于stack exchange,提问作者Jakub
相关产品推荐
相关产品推荐

