| | |
| | | /** 2026 年 1 月当月值列(0 基,即 C 列) */ |
| | | private static final int FIRST_MONTH_COL = 2; |
| | | |
| | | /** 表头行扫描范围(兼容“一页两块”的轨道、轮渡:第 2 块表头在第 11 行) */ |
| | | private static final int HDR_SCAN_ROWS = 15; |
| | | |
| | | /** 回填 1..keepMonths 月台账页,返回写入格数;缺数提示写入 problems */ |
| | | public int fillMonthlyLedger(XSSFWorkbook wb, String period, int year, int keepMonths, List<String> problems) { |
| | | if (wb == null || keepMonths <= 0) return 0; |
| | |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * 「累计 / 累计同比」按表头重建(2026-10-13 新增,根治“累计漏月”)。 |
| | | * |
| | | * <p>背景:当月列由“按月扩列器”插入时只在旧公式末尾追加当月项、不重建;一旦历史上漏过某月 |
| | | * (扩列器早期版本把含巧合日期值的数据行误判成表头行从而整行跳过——2026-09 的 |
| | | * 公交!U8、出租!U6、中口径明细!U5 即此),漏项会永久保留并逐月累积。 |
| | | * |
| | | * <p>本方法在生成收尾时按表头重建公式,底稿里的历史欠账自动补齐,且不再依赖底稿公式本身正确: |
| | | * <ul> |
| | | * <li>「累计」:公式引用全部落在本月度块的“当年当月值列”上时,重写为当年 1..month 各月值之和;</li> |
| | | * <li>「累计同比」:公式引用只用到本月度块的“去年月份列 + 本行累计列”时,分母重写为去年 1..month 各月值之和;</li> |
| | | * <li>已经完整的公式零改动;跨页引用等其它形态不动,只计数告警,避免误伤。</li> |
| | | * </ul> |
| | | * 多块页(轨道、轮渡一页两块)按“该行上方最近表头行”确定所属月份块。 |
| | | */ |
| | | public int repairCumulativeFormulas(XSSFWorkbook wb, int year, int month) { |
| | | if (wb == null || month < 1) return 0; |
| | | int fixed = 0, skipped = 0; |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | Sheet sh = wb.getSheetAt(i); |
| | | if (sh == null || !isMonthLedgerSheet(sh.getSheetName())) continue; |
| | | List<Integer> hdrs = headerRowsOf(sh, year); |
| | | for (int hi = 0; hi < hdrs.size(); hi++) { |
| | | int h = hdrs.get(hi); |
| | | int blockEnd = (hi + 1 < hdrs.size()) ? hdrs.get(hi + 1) - 1 : sh.getLastRowNum(); |
| | | Map<Integer, Integer> cur = new LinkedHashMap<>(); |
| | | Map<Integer, Integer> lastY = new LinkedHashMap<>(); |
| | | monthColsOfRow(sh, h, year, month, cur, lastY); |
| | | if (cur.isEmpty() || lastY.isEmpty()) continue; |
| | | int lastMonthCol = 0; |
| | | for (int c : cur.values()) lastMonthCol = Math.max(lastMonthCol, c); |
| | | Integer cumCol = cumulativeColOfRow(sh, h, lastMonthCol); |
| | | if (cumCol == null) continue; |
| | | Integer cumYoyCol = cumYoyColOfRow(sh, h, cumCol); |
| | | java.util.Set<Integer> curSet = new java.util.HashSet<>(cur.values()); |
| | | java.util.Set<Integer> lastSet = new java.util.HashSet<>(lastY.values()); |
| | | for (int r = h + 1; r <= blockEnd; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | Cell cum = row.getCell(cumCol); |
| | | if (cum != null && cum.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) { |
| | | java.util.Set<Integer> used = sameRowRefCols(cum.getCellFormula(), r + 1); |
| | | if (!used.isEmpty()) { |
| | | if (used.equals(curSet)) { |
| | | // 已完整,零改动 |
| | | } else if (curSet.containsAll(used)) { |
| | | String nf = refsJoined(cur, r + 1); |
| | | if (nf != null && !nf.equals(cum.getCellFormula())) { |
| | | cum.setCellFormula(nf); |
| | | fixed++; |
| | | } |
| | | } else { |
| | | skipped++; |
| | | } |
| | | } |
| | | } |
| | | if (cumYoyCol != null) { |
| | | Cell cy = row.getCell(cumYoyCol); |
| | | if (cy != null && cy.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) { |
| | | java.util.Set<Integer> used = sameRowRefCols(cy.getCellFormula(), r + 1); |
| | | used.remove(cumCol); |
| | | if (!used.isEmpty()) { |
| | | if (used.equals(lastSet)) { |
| | | // 已完整,零改动 |
| | | } else if (lastSet.containsAll(used)) { |
| | | String den = refsJoined(lastY, r + 1); |
| | | if (den != null) { |
| | | String nf = cellRef(cumCol, r + 1) + "/SUM(" + den + ")-1"; |
| | | if (!nf.equals(cy.getCellFormula())) { |
| | | cy.setCellFormula(nf); |
| | | fixed++; |
| | | } |
| | | } |
| | | } else { |
| | | skipped++; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | if (fixed > 0 && log != null) { |
| | | log.info("汇总工作簿:累计/累计同比按表头重建 {} 格(第 {} 月;形态不符跳过 {} 格)", fixed, month, skipped); |
| | | } else if (skipped > 0 && log != null) { |
| | | log.warn("汇总工作簿:{} 格累计/累计同比公式形态不符,未自动重建(需人工确认)", skipped); |
| | | } |
| | | return fixed; |
| | | } |
| | | |
| | | /** 表头行(0 基):先取“月份格最多的那一行”为基准,只有这些列上过半是月份格的行才算表头行(同扩列器口径) */ |
| | | private List<Integer> headerRowsOf(Sheet sh, int year) { |
| | | int last = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + HDR_SCAN_ROWS); |
| | | List<Integer> base = new ArrayList<>(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | List<Integer> cols = monthHeaderCols(sh.getRow(r), year); |
| | | if (cols.size() > base.size()) base = cols; |
| | | } |
| | | List<Integer> rows = new ArrayList<>(); |
| | | if (base.size() < 2) return rows; |
| | | int need = Math.max(2, (base.size() + 1) / 2); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | int hit = 0; |
| | | for (int c : base) if (yearMonthOf(row.getCell(c), year) != null) hit++; |
| | | if (hit >= need) rows.add(r); |
| | | } |
| | | return rows; |
| | | } |
| | | |
| | | private List<Integer> monthHeaderCols(Row row, int year) { |
| | | List<Integer> cols = new ArrayList<>(); |
| | | if (row == null) return cols; |
| | | for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) { |
| | | if (yearMonthOf(row.getCell(c), year) != null) cols.add(c); |
| | | } |
| | | return cols; |
| | | } |
| | | |
| | | /** 表头行 -> 当年 1..month 月值列 / 去年 1..month 月值列(同月取最左一列) */ |
| | | private void monthColsOfRow(Sheet sh, int hdrRow, int year, int month, |
| | | Map<Integer, Integer> cur, Map<Integer, Integer> lastY) { |
| | | Row row = sh.getRow(hdrRow); |
| | | if (row == null) return; |
| | | for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) { |
| | | Integer ym = yearMonthOf(row.getCell(c), year); |
| | | if (ym == null) continue; |
| | | int y = ym / 100, m = ym % 100; |
| | | if (m < 1 || m > month) continue; |
| | | if (y == year && !cur.containsKey(m)) cur.put(m, c); |
| | | else if (y == year - 1 && !lastY.containsKey(m)) lastY.put(m, c); |
| | | } |
| | | } |
| | | |
| | | /** 该表头行「累计」列(0 基):最后一个月份列右侧 3 列内含“累计”且不含“同比”的表头格 */ |
| | | private Integer cumulativeColOfRow(Sheet sh, int hdrRow, int lastMonthCol) { |
| | | Row row = sh.getRow(hdrRow); |
| | | if (row == null) return null; |
| | | int end = Math.min(row.getLastCellNum() - 1, lastMonthCol + 3); |
| | | for (int c = lastMonthCol + 1; c <= end; c++) { |
| | | String t = text(row.getCell(c)); |
| | | if (t == null) continue; |
| | | t = t.trim(); |
| | | if (t.contains("累计") && !t.contains("同比") && !YM_RANGE_CUM.matcher(t).matches()) return c; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** 「累计」右邻(或再右一格)是否为「累计同比」 */ |
| | | private Integer cumYoyColOfRow(Sheet sh, int hdrRow, int cumCol) { |
| | | Row row = sh.getRow(hdrRow); |
| | | if (row == null) return null; |
| | | String t = text(row.getCell(cumCol + 1)); |
| | | if (t != null && t.trim().contains("同比")) return cumCol + 1; |
| | | String t2 = text(row.getCell(cumCol + 2)); |
| | | return (t2 != null && t2.trim().contains("同比")) ? cumCol + 2 : null; |
| | | } |
| | | |
| | | /** 公式中“引用同一行”的列号集合(0 基);SUM 等函数名后无数字,不会被误匹配 */ |
| | | private java.util.Set<Integer> sameRowRefCols(String formula, int row1) { |
| | | java.util.Set<Integer> cols = new java.util.HashSet<>(); |
| | | if (formula == null) return cols; |
| | | java.util.regex.Matcher m = A1_REF.matcher(formula); |
| | | while (m.find()) { |
| | | if (Integer.parseInt(m.group(4)) == row1) cols.add(colIndexOf(m.group(2))); |
| | | } |
| | | return cols; |
| | | } |
| | | |
| | | /** 把 {月->列} 按月份顺序拼成 "C5+E5+G5..." */ |
| | | private String refsJoined(Map<Integer, Integer> monthToCol, int row1) { |
| | | StringBuilder sb = new StringBuilder(); |
| | | for (Map.Entry<Integer, Integer> e : new java.util.TreeMap<>(monthToCol).entrySet()) { |
| | | if (sb.length() > 0) sb.append('+'); |
| | | sb.append(cellRef(e.getValue(), row1)); |
| | | } |
| | | return sb.length() == 0 ? null : sb.toString(); |
| | | } |
| | | |
| | | /** 该页「累计 / 累计同比」两列(0 基),按表头前 5 行找 “累计” 或 “{year}年累计”,且其右列含 “同比” */ |
| | | private int[] cumulativeCols(Sheet sh, int year) { |
| | | int last = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + 4); |