|
|
@@ -0,0 +1,419 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+import com.etotem.cfc.entity.IndicatorDefinition;
|
|
|
+import com.etotem.cfc.entity.IndicatorValue;
|
|
|
+import com.etotem.cfc.mapper.IndicatorValueMapper;
|
|
|
+import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.sql.Date;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 指标变化语义回归测试(T02 验收标准第 7 条 · §1.2.3 的 20 条语义用例)。
|
|
|
+ *
|
|
|
+ * <p><b>为什么必须固化</b>:`goodDirection` / `refRange` 是<b>字典配置驱动</b>的,
|
|
|
+ * 后续运营改字典会静默改变判定结果。一次性人工核验挡不住回归 ——
|
|
|
+ * 本文件是这 20 条的可重复执行版本,任何对 {@code computeChange} 或字典的改动都会跑它。
|
|
|
+ *
|
|
|
+ * <p><b>覆盖矩阵</b>:
|
|
|
+ * <ul>
|
|
|
+ * <li>全部 5 个 trendCode 分支:improved / worsened / flat / first_record / pending_backfill</li>
|
|
|
+ * <li>in_range 四分支:区外→区内(improved) / 区内→区外(worsened) / 都在区内(flat) / 都在区外(靠近=improved)</li>
|
|
|
+ * <li>微小基数(deltaPct=null 但 delta 有值)</li>
|
|
|
+ * <li>基数为 0(deltaPct=null)</li>
|
|
|
+ * <li>单位跨期不一致(deltaPct=null + unitMismatch=true)</li>
|
|
|
+ * <li>非数值(movement=changed/unchanged,trendCode 恒 flat,不给颜色)</li>
|
|
|
+ * <li>跨期跳比(skipGaps=2)与相邻两期(skipGaps=0)</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>放行标准</b>:20 条全绿。方向判反(把恶化标成改善)0 容忍。
|
|
|
+ */
|
|
|
+class IndicatorChangeServiceSemanticTest {
|
|
|
+
|
|
|
+ private IndicatorChangeService service;
|
|
|
+ private IndicatorValueMapper valueMapper;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ // LambdaQueryWrapper.select(SFunction) 需要实体元数据(MP 3.5.3 lambda cache),
|
|
|
+ // 无 Spring 容器时必须手动初始化,否则抛 "can not find lambda cache for this entity"
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
|
|
+ IndicatorValue.class);
|
|
|
+ service = new IndicatorChangeService();
|
|
|
+ valueMapper = mock(IndicatorValueMapper.class);
|
|
|
+ ReflectionTestUtils.setField(service, "valueMapper", valueMapper);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 工具 ----------
|
|
|
+
|
|
|
+ private IndicatorValue num(String val, String unit, String status, String refSnapshot) {
|
|
|
+ IndicatorValue v = new IndicatorValue();
|
|
|
+ v.setValue(val);
|
|
|
+ v.setNumericValue(new BigDecimal(val));
|
|
|
+ v.setUnit(unit);
|
|
|
+ v.setStatus(status);
|
|
|
+ v.setRefRangeSnapshot(refSnapshot);
|
|
|
+ v.setReportDate(Date.valueOf("2026-10-01"));
|
|
|
+ return v;
|
|
|
+ }
|
|
|
+
|
|
|
+ private IndicatorValue text(String val, String status) {
|
|
|
+ IndicatorValue v = new IndicatorValue();
|
|
|
+ v.setValue(val);
|
|
|
+ v.setNumericValue(null);
|
|
|
+ v.setStatus(status);
|
|
|
+ v.setReportDate(Date.valueOf("2026-10-01"));
|
|
|
+ return v;
|
|
|
+ }
|
|
|
+
|
|
|
+ private IndicatorDefinition def(String goodDirection, String valueType, String refRange, String unit) {
|
|
|
+ IndicatorDefinition d = new IndicatorDefinition();
|
|
|
+ d.setGoodDirection(goodDirection);
|
|
|
+ d.setValueType(valueType);
|
|
|
+ d.setRefRange(refRange);
|
|
|
+ d.setUnit(unit);
|
|
|
+ return d;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================================================================
|
|
|
+ // 语义用例 01-05:明确 goodDirection 的方向判定(改善 / 恶化)
|
|
|
+ // ==================================================================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 01:LDL-C 降 → improved(lower_is_better)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic01_ldlC_decrease_isImproved() {
|
|
|
+ IndicatorDefinition d = def("lower_is_better", "numeric", "0.00-3.37", "mmol/L");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("4.10", "mmol/L", "偏高", "0.00-3.37"),
|
|
|
+ num("3.62", "mmol/L", "偏高", "0.00-3.37"));
|
|
|
+ assertEquals("improved", r.getTrendCode());
|
|
|
+ assertEquals("改善", r.getTrendLabel());
|
|
|
+ assertEquals("down", r.getMovement());
|
|
|
+ assertEquals(-0.48, r.getDelta().doubleValue(), 1e-9);
|
|
|
+ assertEquals(-11.7, r.getDeltaPct().doubleValue(), 1e-9);
|
|
|
+ assertTrue(r.isComparable());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 02:HDL-C 升 → improved(higher_is_better)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic02_hdlC_increase_isImproved() {
|
|
|
+ IndicatorDefinition d = def("higher_is_better", "numeric", "1.00-1.60", "mmol/L");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("1.20", "mmol/L", "正常", "1.00-1.60"),
|
|
|
+ num("1.45", "mmol/L", "正常", "1.00-1.60"));
|
|
|
+ assertEquals("improved", r.getTrendCode());
|
|
|
+ assertEquals("改善", r.getTrendLabel());
|
|
|
+ assertEquals("up", r.getMovement());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 03:血糖 升 → worsened(lower_is_better)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic03_glucose_increase_isWorsened() {
|
|
|
+ IndicatorDefinition d = def("lower_is_better", "numeric", "3.90-6.10", "mmol/L");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("5.20", "mmol/L", "正常", "3.90-6.10"),
|
|
|
+ num("6.50", "mmol/L", "偏高", "3.90-6.10"));
|
|
|
+ assertEquals("worsened", r.getTrendCode());
|
|
|
+ assertEquals("恶化", r.getTrendLabel());
|
|
|
+ assertEquals("up", r.getMovement());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 04:higher_is_better 指标下降 → worsened(反向分支,防止字典方向写反)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic04_higherIsBetter_decrease_isWorsened() {
|
|
|
+ IndicatorDefinition d = def("higher_is_better", "numeric", "1.00-5.00", "U");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("4.00", "U", "正常", "1.00-5.00"),
|
|
|
+ num("2.00", "U", "偏低", "1.00-5.00"));
|
|
|
+ assertEquals("worsened", r.getTrendCode());
|
|
|
+ assertEquals("恶化", r.getTrendLabel());
|
|
|
+ assertEquals("down", r.getMovement());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 05:同型半胱氨酸 降 → improved(lower_is_better)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic05_homocysteine_decrease_isImproved() {
|
|
|
+ IndicatorDefinition d = def("lower_is_better", "numeric", "<15.0", "μmol/L");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("12.0", "μmol/L", "正常", "<15.0"),
|
|
|
+ num("9.8", "μmol/L", "正常", "<15.0"));
|
|
|
+ assertEquals("improved", r.getTrendCode());
|
|
|
+ assertEquals("改善", r.getTrendLabel());
|
|
|
+ assertEquals("down", r.getMovement());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================================================================
|
|
|
+ // 语义用例 06-10:in_range_is_better 四分支(修 PRD S10:不再恒为中性)
|
|
|
+ // ==================================================================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 06:BMI 区间内移动 → flat(in_range 四分支之"都在区内")。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic06_bmi_moveWithinRange_isFlat() {
|
|
|
+ IndicatorDefinition d = def("in_range_is_better", "numeric", "18.50-24.00", "kg/m²");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("22.00", "kg/m²", "正常", "18.50-24.00"),
|
|
|
+ num("22.60", "kg/m²", "正常", "18.50-24.00"));
|
|
|
+ assertEquals("flat", r.getTrendCode());
|
|
|
+ assertEquals("基本持平", r.getTrendLabel());
|
|
|
+ assertEquals("up", r.getMovement());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 07:BMI 入区间 → improved(in_range 四分支之"区外→区内")。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic07_bmi_enterRange_isImproved() {
|
|
|
+ IndicatorDefinition d = def("in_range_is_better", "numeric", "18.50-24.00", "kg/m²");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("25.50", "kg/m²", "偏高", "18.50-24.00"),
|
|
|
+ num("23.20", "kg/m²", "正常", "18.50-24.00"));
|
|
|
+ assertEquals("improved", r.getTrendCode());
|
|
|
+ assertEquals("改善", r.getTrendLabel());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 08:BMI 出区间 → worsened(in_range 四分支之"区内→区外")。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic08_bmi_exitRange_isWorsened() {
|
|
|
+ IndicatorDefinition d = def("in_range_is_better", "numeric", "18.50-24.00", "kg/m²");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("23.20", "kg/m²", "正常", "18.50-24.00"),
|
|
|
+ num("25.50", "kg/m²", "偏高", "18.50-24.00"));
|
|
|
+ assertEquals("worsened", r.getTrendCode());
|
|
|
+ assertEquals("恶化", r.getTrendLabel());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 09:白细胞 升 → worsened(in_range,出区间上界)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic09_whiteBloodCell_exitAboveUpper_isWorsened() {
|
|
|
+ IndicatorDefinition d = def("in_range_is_better", "numeric", "3.50-9.50", "10^9/L");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("8.20", "10^9/L", "正常", "3.50-9.50"),
|
|
|
+ num("11.30", "10^9/L", "偏高", "3.50-9.50"));
|
|
|
+ assertEquals("worsened", r.getTrendCode());
|
|
|
+ assertEquals("恶化", r.getTrendLabel());
|
|
|
+ assertEquals("up", r.getMovement());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 10:in_range 始终在区外但靠近区间 → improved(in_range 四分支之"都在区外·靠近")。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic10_inRange_alwaysOutside_closer_isImproved() {
|
|
|
+ IndicatorDefinition d = def("in_range_is_better", "numeric", "18.50-24.00", "kg/m²");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("26.50", "kg/m²", "偏高", "18.50-24.00"),
|
|
|
+ num("25.00", "kg/m²", "偏高", "18.50-24.00"));
|
|
|
+ assertEquals("improved", r.getTrendCode());
|
|
|
+ assertEquals("改善", r.getTrendLabel());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================================================================
|
|
|
+ // 语义用例 11-14:降级路径(微小基数 / 基数为 0 / 单位不一致 / 非数值)
|
|
|
+ // ==================================================================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 11:微小基数(0.01→0.05)→ deltaPct=null(排最后),但 delta 照常有值。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic11_tinyBase_deltaPctNull_butDeltaPresent() {
|
|
|
+ // refHigh=3.37 → floor=0.0337;before=0.01 低于护栏 → 百分比不适用
|
|
|
+ IndicatorDefinition d = def("higher_is_better", "numeric", "0.00-3.37", "mmol/L");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("0.01", "mmol/L", "正常", "0.00-3.37"),
|
|
|
+ num("0.05", "mmol/L", "正常", "0.00-3.37"));
|
|
|
+ assertNull(r.getDeltaPct(), "微小基数的百分比必须为 null(前端显示 —)");
|
|
|
+ assertNotNull(r.getDelta(), "微小基数的 Δ 绝对值必须保留(用户仍需知道涨跌)");
|
|
|
+ assertEquals(0.04, r.getDelta().doubleValue(), 1e-9);
|
|
|
+ assertEquals("up", r.getMovement(), "箭头方向不受 FLOOR 影响");
|
|
|
+ assertNull(service.rankKeyOf(r), "deltaPct=null → 排序键为 null → 恒排最后");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 12:基数为 0 → deltaPct=null。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic12_zeroBase_deltaPctNull() {
|
|
|
+ IndicatorDefinition d = def("lower_is_better", "numeric", "0.00-3.37", "mmol/L");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("0.00", "mmol/L", "正常", "0.00-3.37"),
|
|
|
+ num("1.00", "mmol/L", "正常", "0.00-3.37"));
|
|
|
+ assertNull(r.getDeltaPct(), "基数为 0 时百分比无意义");
|
|
|
+ assertEquals(1.0, r.getDelta().doubleValue(), 1e-9);
|
|
|
+ assertEquals("up", r.getMovement());
|
|
|
+ assertNull(service.rankKeyOf(r));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 13:单位跨期不一致 → deltaPct=null + unitMismatch=true(delta 保留)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic13_unitMismatch_deltaPctNull_unitMismatchTrue() {
|
|
|
+ IndicatorDefinition d = def("higher_is_better", "numeric", "1.00-5.00", "mg/L");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("3.00", "mg/L", "正常", "1.00-5.00"),
|
|
|
+ num("3.50", "g/L", "正常", "1.00-5.00"));
|
|
|
+ assertTrue(r.isUnitMismatch(), "单位跨期不一致必须被识别");
|
|
|
+ assertNull(r.getDeltaPct(), "单位不一致时百分比不可比");
|
|
|
+ assertEquals(0.50, r.getDelta().doubleValue(), 1e-9, "Δ 绝对值仍保留");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 14:非数值(舌象薄白→厚腻)→ movement=changed + trendCode=flat(不给颜色)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic14_nonNumeric_changed_trendCodeFlat_noColor() {
|
|
|
+ IndicatorDefinition d = def(null, "categorical", null, null);
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ text("薄白", "正常"), text("厚腻", "注意"));
|
|
|
+ assertEquals("changed", r.getMovement());
|
|
|
+ assertEquals("flat", r.getTrendCode(), "非数值永不标改善/恶化(合规红线)");
|
|
|
+ assertFalse(r.isComparable());
|
|
|
+ assertNull(r.getDelta());
|
|
|
+ assertNull(r.getDeltaPct());
|
|
|
+ assertEquals("有变化", r.getTrendLabel());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================================================================
|
|
|
+ // 语义用例 15-16:非数值同值 / 持平阈值
|
|
|
+ // ==================================================================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 15:非数值同值 → movement=unchanged + trendCode=flat。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic15_nonNumeric_unchanged_movementUnchanged() {
|
|
|
+ IndicatorDefinition d = def(null, "categorical", null, null);
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ text("薄白", "正常"), text("薄白", "正常"));
|
|
|
+ assertEquals("unchanged", r.getMovement());
|
|
|
+ assertEquals("flat", r.getTrendCode());
|
|
|
+ assertFalse(r.isComparable());
|
|
|
+ assertEquals("无变化", r.getTrendLabel());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 16:持平阈值(|Δ%| < 2% 且状态未变)→ flat + "基本持平"(箭头保留)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic16_belowFlatThreshold_isFlat_butKeepsArrow() {
|
|
|
+ IndicatorDefinition d = def("higher_is_better", "numeric", "1.00-100.00", "U");
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(d,
|
|
|
+ num("100.0", "U", "正常", "1.00-100.00"),
|
|
|
+ num("101.0", "U", "正常", "1.00-100.00"));
|
|
|
+ assertEquals("flat", r.getTrendCode());
|
|
|
+ assertEquals("基本持平", r.getTrendLabel());
|
|
|
+ assertEquals("up", r.getMovement(), "持平仍保留箭头方向");
|
|
|
+ assertNotNull(r.getDeltaPct());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================================================================
|
|
|
+ // 语义用例 17-18:无上一期两档(first_record / pending_backfill)
|
|
|
+ // ==================================================================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 17:无上一期 + 无更早报告 → first_record。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic17_noPrevious_noOlderReport_firstRecord() {
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(null, null,
|
|
|
+ num("1.00", "U", "正常", null), 0);
|
|
|
+ assertEquals("first_record", r.getTrendCode());
|
|
|
+ assertEquals("首次记录 · 暂无对比", r.getTrendLabel());
|
|
|
+ assertEquals("none", r.getMovement());
|
|
|
+ assertFalse(r.isComparable());
|
|
|
+ assertNull(r.getDelta());
|
|
|
+ assertNull(r.getDeltaPct());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 18:无上一期 + 有 3 份更早报告 → pending_backfill(诚实降级,不是"首次记录")。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic18_noPrevious_has3OlderReports_pendingBackfill() {
|
|
|
+ IndicatorChangeService.ChangeResult r = service.computeChange(null, null,
|
|
|
+ num("1.00", "U", "正常", null), 3);
|
|
|
+ assertEquals("pending_backfill", r.getTrendCode());
|
|
|
+ assertEquals("历史数据整理中,暂无法对比", r.getTrendLabel());
|
|
|
+ assertEquals("none", r.getMovement());
|
|
|
+ assertFalse(r.isComparable());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================================================================
|
|
|
+ // 语义用例 19-20:跨期跳比(skipGaps)
|
|
|
+ // ==================================================================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 19:跨期跳比(中间缺 2 期)→ skipGaps=2。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic19_crossPeriodSkip_twoMissingPeriods_skipGaps2() {
|
|
|
+ Date from = Date.valueOf("2026-03-14");
|
|
|
+ Date to = Date.valueOf("2026-10-08");
|
|
|
+ // 全部报告期:03-14 / 05-02 / 07-15 / 08-12 / 10-08(中间缺 05-02 与 07-15)
|
|
|
+ List<IndicatorValue> allPeriods = new ArrayList<>();
|
|
|
+ allPeriods.add(dateOnly(from));
|
|
|
+ allPeriods.add(dateOnly(Date.valueOf("2026-05-02")));
|
|
|
+ allPeriods.add(dateOnly(Date.valueOf("2026-07-15")));
|
|
|
+ allPeriods.add(dateOnly(Date.valueOf("2026-08-12")));
|
|
|
+ allPeriods.add(dateOnly(to));
|
|
|
+ // 该指标在中间有值的期:只有 08-12(1 期)→ reportsBetween=3, obsBetween=1 → skipGaps=2
|
|
|
+ List<IndicatorValue> obsBetween = new ArrayList<>();
|
|
|
+ obsBetween.add(dateOnly(Date.valueOf("2026-08-12")));
|
|
|
+ when(valueMapper.selectList(any())).thenReturn(allPeriods, obsBetween);
|
|
|
+
|
|
|
+ long skipGaps = service.computeSkipGaps(88L, "gut_flora", 512L, from, to);
|
|
|
+ assertEquals(2L, skipGaps);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 语义 20:相邻两期(中间无缺期)→ skipGaps=0。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void semantic20_adjacentPeriods_skipGaps0() {
|
|
|
+ Date from = Date.valueOf("2026-08-12");
|
|
|
+ Date to = Date.valueOf("2026-10-08");
|
|
|
+ List<IndicatorValue> allPeriods = new ArrayList<>();
|
|
|
+ allPeriods.add(dateOnly(from));
|
|
|
+ allPeriods.add(dateOnly(to));
|
|
|
+ List<IndicatorValue> obsBetween = new ArrayList<>();
|
|
|
+ when(valueMapper.selectList(any())).thenReturn(allPeriods, obsBetween);
|
|
|
+
|
|
|
+ long skipGaps = service.computeSkipGaps(88L, "gut_flora", 512L, from, to);
|
|
|
+ assertEquals(0L, skipGaps);
|
|
|
+ }
|
|
|
+
|
|
|
+ private IndicatorValue dateOnly(Date d) {
|
|
|
+ IndicatorValue v = new IndicatorValue();
|
|
|
+ v.setReportDate(d);
|
|
|
+ return v;
|
|
|
+ }
|
|
|
+}
|