| | |
| | | package com.trafficaudit.reportexport.calc; |
| | | |
| | | import com.trafficaudit.common.util.RegionUtil; |
| | | import com.trafficaudit.reportexport.calc.FreightCalc.FreightMatrix; |
| | | import com.trafficaudit.reportexport.calc.FreightCalc.FreightMetrics; |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | | import org.apache.poi.ss.usermodel.Row; |
| | | import org.apache.poi.ss.usermodel.Sheet; |
| | | import org.apache.poi.xssf.usermodel.XSSFWorkbook; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.annotation.Resource; |
| | |
| | | @Service |
| | | public class SummaryWorkbookFiller { |
| | | |
| | | private static final Logger log = LoggerFactory.getLogger(SummaryWorkbookFiller.class); |
| | | |
| | | @Resource |
| | | private MidCalc midCalc; |
| | | @Resource |
| | | private PaxCalc paxCalc; |
| | | @Resource |
| | | private FreightCalc freightCalc; |
| | | @Resource |
| | | private WycSplitCalc wycSplitCalc; |
| | | |
| | | /** 2026 年 1 月当月值列(0 基,即 C 列) */ |
| | | private static final int FIRST_MONTH_COL = 2; |
| | | |
| | | /** 回填 1..keepMonths 月台账页,返回写入格数;缺数提示写入 problems */ |
| | | public int fillMonthlyLedger(XSSFWorkbook wb, int year, int keepMonths, List<String> problems) { |
| | | public int fillMonthlyLedger(XSSFWorkbook wb, String period, int year, int keepMonths, List<String> problems) { |
| | | if (wb == null || keepMonths <= 0) return 0; |
| | | if (problems == null) problems = new ArrayList<>(); |
| | | String yearPrefix = year + "-"; |
| | |
| | | filled += fillCityRows(wb, "公交", paxCalc.busMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false); |
| | | filled += fillCityRows(wb, "出租", paxCalc.taxiMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false); |
| | | filled += fillTrackFerry(wb, paxCalc.railFerryMonthly(yearPrefix, keepMonths), year, keepMonths, problems); |
| | | filled += fillFreightSheet(wb, period, keepMonths, problems); |
| | | filled += fillWycSheet(wb, period, keepMonths, problems); |
| | | return filled; |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | return filled; |
| | | } |
| | | |
| | | /** 货运页回填:全省块 + 17 市州块的 规上/规下/合计 × 货运量、周转量(逐月现算;当月值列=2+2*(m-1)) */ |
| | | private int fillFreightSheet(XSSFWorkbook wb, String period, int keepMonths, List<String> problems) { |
| | | Sheet sh = wb.getSheet(" 货运"); |
| | | if (sh == null) sh = wb.getSheet("货运"); |
| | | if (sh == null) { |
| | | problems.add("货运页不存在(母版缺失该页),跳过自动回填"); |
| | | return 0; |
| | | } |
| | | int cap = monthColumnCapacity(sh); |
| | | int mMax = Math.min(keepMonths, cap); |
| | | if (mMax <= 0) { |
| | | problems.add("货运页未识别到 2026 年累计列,跳过自动回填"); |
| | | return 0; |
| | | } |
| | | int year = periodYear(period); |
| | | // 块(全省/市州):指标键 -> 行号 |
| | | Map<String, Map<String, Integer>> blockRows = new LinkedHashMap<>(); |
| | | String cur = null; |
| | | for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String a = text(row.getCell(0)); |
| | | if (a != null && !a.trim().isEmpty()) { |
| | | String norm = RegionUtil.normalizeCityName(a.trim()); |
| | | if (FreightCalc.PROVINCE.equals(norm) || RegionUtil.cityList().contains(norm)) { |
| | | cur = norm; |
| | | blockRows.computeIfAbsent(cur, k -> new LinkedHashMap<>()); |
| | | } else { |
| | | cur = null; // 标题行 / 非全省非市州 -> 脱离块 |
| | | } |
| | | } |
| | | if (cur == null) continue; |
| | | String key = freightMetricKey(text(row.getCell(1))); |
| | | if (key == null) continue; |
| | | blockRows.get(cur).putIfAbsent(key, r); |
| | | } |
| | | if (blockRows.isEmpty()) { |
| | | problems.add("货运页未识别到全省/市州行,跳过自动回填"); |
| | | return 0; |
| | | } |
| | | int filled = 0; |
| | | // 只回填目标期当月:1..目标月-1 的值以母版(含同期备份母版还原)为准,避免改写已确认口径 |
| | | int targetM = Math.min(periodMonth(period), mMax); |
| | | for (int m = targetM; m <= targetM; m++) { |
| | | String mp = String.format("%04d-%02d", year, m); |
| | | FreightMatrix mx; |
| | | try { |
| | | mx = freightCalc.calc(mp); |
| | | } catch (Exception e) { |
| | | problems.add("货运页 " + mp + " 取数失败:" + e.getMessage()); |
| | | continue; |
| | | } |
| | | int col = FIRST_MONTH_COL + 2 * (m - 1); |
| | | for (Map.Entry<String, Map<String, Integer>> e : blockRows.entrySet()) { |
| | | FreightMetrics fm = mx.getMonth().get(e.getKey()); |
| | | if (fm == null) continue; |
| | | Map<String, Integer> rows = e.getValue(); |
| | | // 1 月列是公式的“派生行”(全省合计 货运量/周转量、市州 规上+规下周转量)不写值, |
| | | // 由 ensureMonthFormulas 把公式按月右移,保持与母版一致 |
| | | filled += setFreightCell(sh, skipIfDerived(sh, rows.get("totalFreight")), col, fm.getTotalFreight()); |
| | | filled += setFreightCell(sh, skipIfDerived(sh, rows.get("aboveFreight")), col, fm.getAboveFreight()); |
| | | filled += setFreightCell(sh, skipIfDerived(sh, rows.get("belowFreight")), col, fm.getBelowFreight()); |
| | | filled += setFreightCell(sh, skipIfDerived(sh, rows.get("totalTurnover")), col, fm.getTotalTurnover()); |
| | | filled += setFreightCell(sh, skipIfDerived(sh, rows.get("aboveTurnover")), col, fm.getAboveTurnover()); |
| | | filled += setFreightCell(sh, skipIfDerived(sh, rows.get("belowTurnover")), col, fm.getBelowTurnover()); |
| | | } |
| | | } |
| | | return filled; |
| | | } |
| | | |
| | | /** 该行 1 月列(C)是公式 -> 派生行,返回 null 表示不回填数值 */ |
| | | private Integer skipIfDerived(Sheet sh, Integer r) { |
| | | if (r == null) return null; |
| | | Row row = sh.getRow(r); |
| | | if (row == null) return r; |
| | | Cell c = row.getCell(FIRST_MONTH_COL); |
| | | if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) return null; |
| | | return r; |
| | | } |
| | | |
| | | private int setFreightCell(Sheet sh, Integer r, int col, Double v) { |
| | | if (r == null || v == null) return 0; |
| | | return setNumeric(sh, r, col, v); |
| | | } |
| | | |
| | | /** 货运页 B 列指标文案 -> 指标键(规上+规下 / 规上 / 规下 × 货运量 / 周转量) */ |
| | | private String freightMetricKey(String b) { |
| | | if (b == null) return null; |
| | | String s = b.replaceAll("\\s+", ""); |
| | | if (s.isEmpty()) return null; |
| | | boolean turnover = s.contains("周转量"); |
| | | if (s.contains("规上+规下")) return turnover ? "totalTurnover" : "totalFreight"; |
| | | if (s.contains("规上")) return turnover ? "aboveTurnover" : "aboveFreight"; |
| | | if (s.contains("规下")) return turnover ? "belowTurnover" : "belowFreight"; |
| | | return turnover ? "totalTurnover" : "totalFreight"; // 全省块的“货运量/货物周转量”= 合计 |
| | | } |
| | | |
| | | /** 网约车页回填:17 市州 的 客运量/周转量/其中城市内客运量/其中城市内周转量(逐月现算) |
| | | * 全省行(=17 市州求和公式)由 ensureMonthFormulas 按月扩列,不在此处写值。 */ |
| | | private int fillWycSheet(XSSFWorkbook wb, String period, int keepMonths, List<String> problems) { |
| | | Sheet sh = wb.getSheet("网约车"); |
| | | if (sh == null) { |
| | | problems.add("网约车页不存在(母版缺失该页),跳过自动回填"); |
| | | return 0; |
| | | } |
| | | int cap = monthColumnCapacity(sh); |
| | | int mMax = Math.min(keepMonths, cap); |
| | | if (mMax <= 0) { |
| | | problems.add("网约车页未识别到 2026 年累计列,跳过自动回填"); |
| | | return 0; |
| | | } |
| | | int year = periodYear(period); |
| | | Map<String, Map<String, Integer>> cityMetricRows = new LinkedHashMap<>(); |
| | | String cur = null; |
| | | for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String a = text(row.getCell(0)); |
| | | if (a != null && !a.trim().isEmpty()) { |
| | | String norm = RegionUtil.normalizeCityName(a.trim()); |
| | | cur = RegionUtil.cityList().contains(norm) ? norm : null; |
| | | if (cur != null) cityMetricRows.computeIfAbsent(cur, k -> new LinkedHashMap<>()); |
| | | } |
| | | if (cur == null) continue; |
| | | String key = wycMetricKey(text(row.getCell(1))); |
| | | if (key == null) continue; |
| | | cityMetricRows.get(cur).putIfAbsent(key, r); |
| | | } |
| | | if (cityMetricRows.isEmpty()) { |
| | | problems.add("网约车页未识别到市州行,跳过自动回填"); |
| | | return 0; |
| | | } |
| | | int filled = 0; |
| | | // 只回填目标期当月(历史月以母版/历史生成件为准) |
| | | int targetM = Math.min(periodMonth(period), mMax); |
| | | for (int m = targetM; m <= targetM; m++) { |
| | | String mp = String.format("%04d-%02d", year, m); |
| | | WycSplitCalc.WycResult wr; |
| | | try { |
| | | wr = wycSplitCalc.calc(mp); |
| | | } catch (Exception e) { |
| | | if (m == mMax) { |
| | | problems.add("网约车页 " + mp + " 源数据缺失,当月未回填:" + e.getMessage()); |
| | | } |
| | | continue; |
| | | } |
| | | int col = FIRST_MONTH_COL + 2 * (m - 1); |
| | | for (Map.Entry<String, Map<String, Integer>> e : cityMetricRows.entrySet()) { |
| | | WycSplitCalc.WycMetrics wm = wr.getByCity().get(e.getKey()); |
| | | if (wm == null) continue; |
| | | Map<String, Integer> rows = e.getValue(); |
| | | filled += setFreightCell(sh, rows.get("pax"), col, wm.getTotalPax()); |
| | | filled += setFreightCell(sh, rows.get("turnover"), col, wm.getTotalTurnover()); |
| | | filled += setFreightCell(sh, rows.get("cityPax"), col, wm.getCityPax()); |
| | | filled += setFreightCell(sh, rows.get("cityTurnover"), col, wm.getCityTurnover()); |
| | | } |
| | | } |
| | | return filled; |
| | | } |
| | | |
| | | /** 网约车页 B 列指标文案 -> 指标键 */ |
| | | private String wycMetricKey(String b) { |
| | | if (b == null) return null; |
| | | String s = b.replaceAll("\\s+", ""); |
| | | if (s.isEmpty()) return null; |
| | | boolean city = s.contains("城市内"); |
| | | boolean turnover = s.contains("周转量"); |
| | | if (city) return turnover ? "cityTurnover" : "cityPax"; |
| | | return turnover ? "turnover" : "pax"; |
| | | } |
| | | |
| | | /** 轨道轮渡页(轨道=武汉/黄石;轮渡仅武汉)专用回填 */ |
| | |
| | | return -1; |
| | | } |
| | | |
| | | /** 写数值:格不存在则建格并复制同行 1 月列样式;返回 1 */ |
| | | /** 写数值:格不存在则建格;格带日期样式(如公交/出租 8 月列继承了表头“yyyy年m月”)时改回同行 1 月列的数值样式,避免数值显示成日期 */ |
| | | private int setNumeric(Sheet sh, int r, int col, double val) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) row = sh.createRow(r); |
| | | Cell c = row.getCell(col); |
| | | if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) { |
| | | return 0; // 母版本来就靠公式算的行(合计/同页求和)保持公式,交 Excel 重算 |
| | | } |
| | | Cell ref = row.getCell(FIRST_MONTH_COL); |
| | | boolean needStyle = false; |
| | | if (c == null) { |
| | | c = row.createCell(col); |
| | | Cell ref = row.getCell(FIRST_MONTH_COL); |
| | | if (ref != null && ref.getCellStyle() != null) c.setCellStyle(ref.getCellStyle()); |
| | | needStyle = true; |
| | | } else if (c.getCellType() == org.apache.poi.ss.usermodel.CellType.BLANK |
| | | || (c.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC |
| | | && org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(c))) { |
| | | // 母版把 8 月列的表头日期格式带到了数据格(或有格式无值),写值前改回同行 1 月列的数字格式 |
| | | needStyle = true; |
| | | } |
| | | if (needStyle && ref != null && ref.getCellStyle() != null) { |
| | | c.setCellStyle(ref.getCellStyle()); |
| | | } |
| | | c.setCellValue(val); |
| | | return 1; |
| | |
| | | return row == null ? null : cityOf(row.getCell(0)); |
| | | } |
| | | |
| | | /** 月份容量:在表头(前 5 行)找“2026…累计”列,容量=(累计列-1 月列)/2 */ |
| | | /** |
| | | * 为第 month 月补“当月值”列公式:取同一行最近一个月仍为公式的值列,整列右移过来 |
| | | * (如公交 7 月列 O 的 =O9+O13+... -> 8 月列 Q 的 =Q9+Q13+...)。 |
| | | * 只补空单元格;已有数值(货运全省块/市州块由数据回填)或已有公式的格不动。 |
| | | */ |
| | | public int ensureMonthFormulas(XSSFWorkbook wb, int year, int month) { |
| | | if (wb == null || month <= 1) return 0; |
| | | int targetCol = FIRST_MONTH_COL + 2 * (month - 1); |
| | | int written = 0; |
| | | int carried = 0; |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | Sheet sh = wb.getSheetAt(i); |
| | | if (sh == null || !isMonthLedgerSheet(sh.getSheetName())) continue; |
| | | boolean derivedSheet = isDerivedFormulaSheet(sh.getSheetName()); |
| | | for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | if (hasContent(row.getCell(targetCol))) continue; |
| | | Cell srcCell = null; |
| | | int srcMonth = -1; |
| | | for (int s = month - 1; s >= 1; s--) { |
| | | Cell c = row.getCell(FIRST_MONTH_COL + 2 * (s - 1)); |
| | | if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) { |
| | | srcCell = c; |
| | | srcMonth = s; |
| | | break; |
| | | } |
| | | } |
| | | if (srcCell == null) { |
| | | if (!derivedSheet) continue; |
| | | // 派生页(城市客运/中口径明细/公路总客运)里「各月都是同一个常量」的结构性常量行 |
| | | // (如中口径明细 武汉 城际城乡巡游出租=0),按月沿用该常量,避免当月值列空着导致累计缺项 |
| | | double struct = structuralConstant(row, month); |
| | | if (Double.isNaN(struct)) continue; |
| | | Cell target = row.getCell(targetCol); |
| | | if (target == null) target = row.createCell(targetCol); |
| | | Cell prev = row.getCell(FIRST_MONTH_COL + 2 * (month - 2)); |
| | | if (prev != null && prev.getCellStyle() != null) target.setCellStyle(prev.getCellStyle()); |
| | | target.setCellValue(struct); |
| | | carried++; |
| | | written++; |
| | | continue; |
| | | } |
| | | String f = srcCell.getCellFormula(); |
| | | if (f == null || f.isEmpty()) continue; |
| | | Cell target = row.getCell(targetCol); |
| | | if (target == null) target = row.createCell(targetCol); |
| | | if (srcCell.getCellStyle() != null) target.setCellStyle(srcCell.getCellStyle()); |
| | | target.setCellFormula(shiftFormulaColumns(f, 2 * (month - srcMonth))); |
| | | written++; |
| | | } |
| | | } |
| | | if (carried > 0 && log != null) { |
| | | log.info("汇总工作簿:派生页结构性常量行按月沿用 {} 格(第 {} 月)", carried, month); |
| | | } |
| | | return written; |
| | | } |
| | | |
| | | /** 该行 1..month-1 月的当月值列是否全为同一个数值常量(结构性常量行,如各月恒为 0) */ |
| | | private double structuralConstant(Row row, int month) { |
| | | if (row == null || month <= 2) return Double.NaN; |
| | | Double v = null; |
| | | for (int s = 1; s < month; s++) { |
| | | Cell c = row.getCell(FIRST_MONTH_COL + 2 * (s - 1)); |
| | | if (c == null || c.getCellType() != org.apache.poi.ss.usermodel.CellType.NUMERIC) return Double.NaN; |
| | | double d = c.getNumericCellValue(); |
| | | if (v == null) v = d; |
| | | else if (Math.abs(d - v) > 1e-9) return Double.NaN; |
| | | } |
| | | return v == null ? Double.NaN : v; |
| | | } |
| | | |
| | | /** 派生页(当月值全部来自其他页/常量,纯公式驱动):城市客运 / 中口径明细 / 公路总客运 / 中口径客运量 */ |
| | | private boolean isDerivedFormulaSheet(String name) { |
| | | if (name == null) return false; |
| | | String n = name.trim(); |
| | | return "城市客运".equals(n) || "中口径明细".equals(n) || "公路总客运".equals(n) || "中口径客运量".equals(n); |
| | | } |
| | | |
| | | /** 该格是否已有内容(数值/文本/公式);仅带样式的空壳(如母版里继承表头日期格式的格子)视为空 */ |
| | | private boolean hasContent(Cell c) { |
| | | if (c == null) return false; |
| | | switch (c.getCellType()) { |
| | | case BLANK: return false; |
| | | case STRING: return c.getStringCellValue() != null && !c.getStringCellValue().trim().isEmpty(); |
| | | default: return true; |
| | | } |
| | | } |
| | | |
| | | /** 0 基列号 + 1 基行号 -> "Q5" */ |
| | | private String cellRef(int col0, int row1) { |
| | | StringBuilder sb = new StringBuilder(); |
| | | int n = col0 + 1; |
| | | while (n > 0) { |
| | | int rem = (n - 1) % 26; |
| | | sb.insert(0, (char) ('A' + rem)); |
| | | n = (n - 1) / 26; |
| | | } |
| | | return sb.toString() + row1; |
| | | } |
| | | |
| | | /** 从 "2026-08" 取月份(解析失败返回 1) */ |
| | | private int periodMonth(String period) { |
| | | if (period != null && period.length() >= 7) { |
| | | try { return Integer.parseInt(period.substring(5, 7)); } catch (Exception ignore) { } |
| | | } |
| | | return 1; |
| | | } |
| | | |
| | | /** 从 "2026-08" 取年份 */ |
| | | private int periodYear(String period) { |
| | | if (period != null && period.length() >= 4) { |
| | | try { return Integer.parseInt(period.substring(0, 4)); } catch (Exception ignore) { } |
| | | } |
| | | return 0; |
| | | } |
| | | |
| | | /** |
| | | * 为第 m 月补“与去年同比”公式:把第 m-1 月同比列的公式整体右移 2 列 |
| | | * (如公交 7 月 P5 公式 =O5/AH5-1 -> 8 月 R5 =Q5/AJ5-1), |
| | | * 因此同比分母取的就是汇总表 2025 年同月列(用户口径)。 |
| | | */ |
| | | public int ensureMonthYoyFormulas(XSSFWorkbook wb, int year, int month) { |
| | | if (wb == null || month <= 1) return 0; |
| | | int valueCol = FIRST_MONTH_COL + 2 * (month - 1); |
| | | int yoyCol = valueCol + 1; |
| | | int prevYoyCol = yoyCol - 2; |
| | | int written = 0; |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | Sheet sh = wb.getSheetAt(i); |
| | | if (sh == null) continue; |
| | | if (!isMonthLedgerSheet(sh.getSheetName())) continue; |
| | | for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | Cell prev = row.getCell(prevYoyCol); |
| | | if (prev == null || prev.getCellType() != org.apache.poi.ss.usermodel.CellType.FORMULA) continue; |
| | | Cell target = row.getCell(yoyCol); |
| | | if (target != null && target.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) continue; |
| | | String f = prev.getCellFormula(); |
| | | if (f == null || f.isEmpty()) continue; |
| | | String shifted = shiftFormulaColumns(f, 2); |
| | | if (target == null) target = row.createCell(yoyCol); |
| | | // 母版把新月份的同比列做成了普通数值格式(显示成小数),统一沿用上月同比列的样式(百分数) |
| | | if (prev.getCellStyle() != null) target.setCellStyle(prev.getCellStyle()); |
| | | try { |
| | | target.setCellFormula(shifted); |
| | | written++; |
| | | } catch (Exception e) { |
| | | throw new IllegalStateException(String.format( |
| | | "同比公式补写失败:sheet=%s cell=%s 原式=%s 目标式=%s", |
| | | sh.getSheetName(), cellRef(yoyCol, r + 1), f, shifted), e); |
| | | } |
| | | } |
| | | } |
| | | return written; |
| | | } |
| | | |
| | | /** 随月扩列的长表页(表头在 1..3 行、月份组为“当月值+同比”交替) */ |
| | | private boolean isMonthLedgerSheet(String name) { |
| | | if (name == null) return false; |
| | | String n = name.trim(); |
| | | return "货运".equals(n) || "公路总客运".equals(n) || "班线包车".equals(n) || "城市客运".equals(n) |
| | | || "公交".equals(n) || "出租".equals(n) || "网约车".equals(n) |
| | | || "轨道、轮渡".equals(n) || "轨道轮渡".equals(n) |
| | | || "中口径明细".equals(n) || "中口径客运量".equals(n); |
| | | } |
| | | |
| | | /** 公式内所有 A1 形式的列引用右移 delta 列(SUM 等函数名后无数字,不会被匹配) */ |
| | | private String shiftFormulaColumns(String formula, int delta) { |
| | | java.util.regex.Matcher m = java.util.regex.Pattern |
| | | .compile("(?<![A-Za-z0-9_$])([$]?)([A-Z]{1,3})([$]?)([0-9]+)") |
| | | .matcher(formula); |
| | | StringBuffer sb = new StringBuffer(); |
| | | while (m.find()) { |
| | | String col = m.group(2); |
| | | int idx = 0; |
| | | for (int i = 0; i < col.length(); i++) idx = idx * 26 + (col.charAt(i) - 'A' + 1); |
| | | int nidx = idx + delta; |
| | | StringBuilder nc = new StringBuilder(); |
| | | while (nidx > 0) { |
| | | int rem = (nidx - 1) % 26; |
| | | nc.insert(0, (char) ('A' + rem)); |
| | | nidx = (nidx - 1) / 26; |
| | | } |
| | | m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(m.group(1) + nc + m.group(3) + m.group(4))); |
| | | } |
| | | m.appendTail(sb); |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** 月份容量:优先取表头(前 5 行)“2026…累计”列反推;货运页无该文案, |
| | | * 再用表头里最后一个“2026年M月”/日期格式的月列兜底(如货运 Q3=2026-08-01 -> 8) */ |
| | | private int monthColumnCapacity(Sheet sh) { |
| | | int byCumulative = 0; |
| | | int byMonthHeader = 0; |
| | | for (int r = 0; r <= 4 && r <= sh.getLastRowNum(); r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | for (Cell c : row) { |
| | | if (c == null) continue; |
| | | int idx = c.getColumnIndex(); |
| | | if (idx <= FIRST_MONTH_COL || (idx - FIRST_MONTH_COL) % 2 != 0) continue; // 只看“当月值”列 |
| | | String t = text(c); |
| | | if (t != null && t.contains("2026") && t.contains("累计") && c.getColumnIndex() > FIRST_MONTH_COL) { |
| | | return (c.getColumnIndex() - FIRST_MONTH_COL) / 2; |
| | | if (byCumulative == 0 && t != null && t.contains("2026") && t.contains("累计")) { |
| | | byCumulative = (idx - FIRST_MONTH_COL) / 2; |
| | | } |
| | | if (t != null && t.contains("2026") && t.contains("月") && !t.contains("累计") |
| | | && !t.contains("同比")) { |
| | | byMonthHeader = Math.max(byMonthHeader, (idx - FIRST_MONTH_COL) / 2 + 1); |
| | | } else if (c.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC |
| | | && org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(c)) { |
| | | byMonthHeader = Math.max(byMonthHeader, (idx - FIRST_MONTH_COL) / 2 + 1); |
| | | } |
| | | } |
| | | } |
| | | return 7; |
| | | int cap = Math.max(byCumulative, byMonthHeader); |
| | | return cap > 0 ? cap : 7; |
| | | } |
| | | |
| | | private String text(Cell c) { |