| | |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | | import org.apache.poi.ss.usermodel.CellType; |
| | | import org.apache.poi.ss.usermodel.FormulaEvaluator; |
| | | import org.apache.poi.ss.usermodel.Row; |
| | | import org.apache.poi.ss.usermodel.Sheet; |
| | | import org.apache.poi.ss.util.CellRangeAddress; |
| | |
| | | |
| | | private static final int HEADER_SCAN_ROWS = 15; |
| | | |
| | | /** 标题由程序在出表时现写的 7 个页签(母版里的标题文字需按母版期次重写)。 */ |
| | | private static final List<String> TITLE_PAGES = Arrays.asList( |
| | | "货运量排名", "货运周转量排名", "城市汇总", "城市分市州", "中口径排名", "中口径分析", "道路运输周转量"); |
| | | |
| | | /** 标题识别标记(命中其一才改写,避免误伤备注等文字)。 */ |
| | | private static final List<String> TITLE_MARKERS = Arrays.asList( |
| | | "全省分市州累计完成公路货运量情况", "全省分市州完成公路货运量情况", |
| | | "全省分市州累计完成公路货物运输周转量情况", "全省分市州完成公路货物运输周转量情况", |
| | | "全省累计完成城市客运量情况", "全省完成城市客运量情况", |
| | | "分市州1-", "全省分市州累计完成道路客运生产情况", "全省分市州完成道路客运生产情况", |
| | | "中口径客运量及周转量", |
| | | "全省累计完成交通周转量情况", "全省完成交通周转量情况"); |
| | | |
| | | /** 公式里的 A1 引用(可带工作表前缀)。 */ |
| | | private static final Pattern REF = Pattern.compile( |
| | | "(?:('([^']+)'|([A-Za-z_\\u4e00-\\u9fff][A-Za-z0-9_.\\u4e00-\\u9fff\u3001]*))!)?(\\$?)([A-Z]{1,3})(\\$?)(\\d+)"); |
| | |
| | | if (sh == null || expanded.contains(sh.getSheetName())) continue; |
| | | shiftQualifiedRefs(sh, insertColOf); |
| | | } |
| | | recalcAndCache(wb); |
| | | int titleFixed = applyTitles(wb, year, targetMonth); |
| | | if (titleFixed > 0) { |
| | | log.info("扩列:标题按 {}年{}月 重写 {} 格", year, targetMonth, titleFixed); |
| | | } |
| | | wb.setForceFormulaRecalculation(true); |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | wb.write(out); |
| | |
| | | } catch (Exception e) { |
| | | throw new IllegalStateException("汇总大表扩列失败: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | /** 只把 7 个标题页的标题按给定期次重写(用于修正母版里滞后的标题)。 */ |
| | | public byte[] fixTitles(byte[] source, int year, int month) { |
| | | try (XSSFWorkbook wb = new XSSFWorkbook(new ByteArrayInputStream(source))) { |
| | | recalcAndCache(wb); |
| | | int n = applyTitles(wb, year, month); |
| | | wb.setForceFormulaRecalculation(true); |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | wb.write(out); |
| | | log.info("汇总大表标题修正完成:{}年{}月,改写 {} 格", year, month, n); |
| | | return out.toByteArray(); |
| | | } catch (Exception e) { |
| | | throw new IllegalStateException("汇总大表标题修正失败: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | /** 整本重算并把结果写入缓存值:否则母版里只有公式没有值,后续读取会把空值当 0(2026-10-12 修) */ |
| | | private void recalcAndCache(XSSFWorkbook wb) { |
| | | try { |
| | | FormulaEvaluator ev = wb.getCreationHelper().createFormulaEvaluator(); |
| | | ev.clearAllCachedResultValues(); |
| | | ev.evaluateAll(); |
| | | } catch (Exception e) { |
| | | log.warn("扩列:整本重算未完成(公式仍在,Excel 打开会再算):{}", e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** 把 7 个页签的标题文字按「累计=1-M月、当月=M月」重写;命中标记才改,避免误伤备注。 */ |
| | | private int applyTitles(XSSFWorkbook wb, int year, int month) { |
| | | int changed = 0; |
| | | for (String page : TITLE_PAGES) { |
| | | Sheet sh = wb.getSheet(page); |
| | | if (sh == null) continue; |
| | | int lastRow = Math.min(sh.getLastRowNum(), 40); |
| | | for (int r = 0; r <= lastRow; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | for (int c = 0; c <= 1; c++) { |
| | | Cell cell = row.getCell(c); |
| | | if (cell == null || cell.getCellType() != CellType.STRING) continue; |
| | | String t = cell.getStringCellValue(); |
| | | if (t == null || !looksLikeTitle(t)) continue; |
| | | String nt = t.replaceAll("(\\d{4})年", year + "年") |
| | | .replaceAll("(\\d{1,2})月", month + "月"); |
| | | if (!nt.equals(t)) { |
| | | cell.setCellValue(nt); |
| | | changed++; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return changed; |
| | | } |
| | | |
| | | private boolean looksLikeTitle(String t) { |
| | | for (String m : TITLE_MARKERS) { |
| | | if (t.contains(m)) return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** 对单页扩列,返回插入位置(0 基列号)。 */ |
| | |
| | | if (headerRows.contains(r)) continue; |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | cloneDataCell(row, srcVal, insertCol, shiftable, insertCol); |
| | | cloneDataCell(row, srcYoy, insertCol + 1, shiftable, insertCol); |
| | | cloneDataCell(row, srcVal, insertCol, shiftable); |
| | | cloneYoyCell(row, srcYoy, insertCol + 1, insertCol, lastYearSameMonth + 2); |
| | | } |
| | | // 5) 累计列补当月项;累计同比分母补去年同月列 |
| | | int cumCol = insertCol + 2, cumYoyCol = insertCol + 3; |
| | |
| | | return insertCol; |
| | | } |
| | | |
| | | /** 表头行(0 基):含「年+月」表头单元格的行;轨道、轮渡有两个块 → 两行。 */ |
| | | /** |
| | | * 表头行(0 基):轨道、轮渡这类一页两个块的有两行。 |
| | | * |
| | | * <p>2026-10-12 二次修正:先取「月份表头格最多的那一行」作基准,把它的月份列当作标准列, |
| | | * 只有这些列上大半是月份格的行才算表头行。前版只要求「整行 ≥2 个月份格」, |
| | | * 会把数据行里恰好落成日期序列的数值当成表头 → 扩列时把月份日期写进数据行 |
| | | * (2026-09 母版 ` 货运!S11`、`出租!S6` 等 13 格被写成 46266)。 |
| | | */ |
| | | private List<Integer> headerRowsOf(Sheet sh) { |
| | | List<Integer> rows = new ArrayList<>(); |
| | | int lastRow = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + HEADER_SCAN_ROWS); |
| | | for (int r = sh.getFirstRowNum(); r <= lastRow; r++) { |
| | | int first = sh.getFirstRowNum(); |
| | | int lastRow = Math.min(sh.getLastRowNum(), first + HEADER_SCAN_ROWS); |
| | | List<Integer> base = new ArrayList<>(); |
| | | for (int r = first; r <= lastRow; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) { |
| | | if (yearMonthOf(row.getCell(c)) != null) { |
| | | rows.add(r); |
| | | break; |
| | | } |
| | | List<Integer> cols = monthColsOf(row); |
| | | 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 = first; r <= lastRow; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | int hit = 0; |
| | | for (int c : base) { |
| | | if (yearMonthOf(row.getCell(c)) != null) hit++; |
| | | } |
| | | if (hit >= need) rows.add(r); |
| | | } |
| | | return rows; |
| | | } |
| | | |
| | | /** 行内所有「年+月」表头格的列号(0 基) */ |
| | | private List<Integer> monthColsOf(Row row) { |
| | | List<Integer> cols = new ArrayList<>(); |
| | | for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) { |
| | | if (yearMonthOf(row.getCell(c)) != null) cols.add(c); |
| | | } |
| | | return cols; |
| | | } |
| | | |
| | | private Map<Integer, Integer> monthCols(Sheet sh, int year) { |
| | |
| | | } |
| | | } |
| | | |
| | | private void cloneDataCell(Row row, int srcCol, int dstCol, Set<String> shiftable, int insertCol) { |
| | | private void cloneDataCell(Row row, int srcCol, int dstCol, Set<String> shiftable) { |
| | | Cell s = row.getCell(srcCol); |
| | | if (s == null) return; |
| | | Cell d = getOrCreate(row, dstCol); |
| | | d.setBlank(); |
| | | d.setCellStyle(s.getCellStyle()); |
| | | if (s.getCellType() == CellType.FORMULA) { |
| | | d.setCellFormula(shiftRefs(s.getCellFormula(), insertCol, shiftable)); |
| | | d.setCellFormula(shiftRefsAll(s.getCellFormula(), shiftable)); |
| | | } |
| | | } |
| | | |
| | | /** 当月同比列:不克隆上月公式,直接按「本月值 / 去年同月 − 1」重写(去年同月列已按表头定位) */ |
| | | private void cloneYoyCell(Row row, int srcCol, int dstCol, int valCol0, int denCol0) { |
| | | Cell s = row.getCell(srcCol); |
| | | if (s == null) return; |
| | | Cell d = getOrCreate(row, dstCol); |
| | | d.setBlank(); |
| | | d.setCellStyle(s.getCellStyle()); |
| | | if (s.getCellType() == CellType.FORMULA) { |
| | | int excelRow = row.getRowNum() + 1; |
| | | d.setCellFormula(colLetter(valCol0) + excelRow + "/" + colLetter(denCol0) + excelRow + "-1"); |
| | | } |
| | | } |
| | | |
| | | /** 相对复制用的平移:把本页/已扩列页的引用整体 +2(不做列号阈值判断) */ |
| | | private String shiftRefsAll(String formula, Set<String> shiftable) { |
| | | Matcher m = REF.matcher(formula); |
| | | StringBuffer sb = new StringBuffer(); |
| | | while (m.find()) { |
| | | String sheet = m.group(2) != null ? m.group(2) : m.group(3); |
| | | if ((sheet != null && !shiftable.contains(sheet)) || isFunctionName(formula, m)) { |
| | | m.appendReplacement(sb, Matcher.quoteReplacement(m.group(0))); |
| | | continue; |
| | | } |
| | | int colIdx = colIndex(m.group(5)); |
| | | m.appendReplacement(sb, Matcher.quoteReplacement(rebuild(m, sheet, colIdx + 2))); |
| | | } |
| | | m.appendTail(sb); |
| | | return sb.toString(); |
| | | } |
| | | |
| | | private void shiftAllRefs(Sheet sh, int insertCol, Set<String> shiftable) { |
| | | for (int r = 0; r <= sh.getLastRowNum(); r++) { |
| | | Row row = sh.getRow(r); |