表格层级合并单元格

171 阅读5分钟

在数据展示中,我们经常需要对表格进行合并操作,特别是层级合并,以便更清晰地展示具有层次结构的数据。

我实现了递归的算法,用 AI 优化为基于队列的形式。

最后用 AI 生成了一个测试工具

效果

算法实现

算法一:基于队列的迭代实现

第一种算法采用迭代方式,使用队列来管理需要处理的区间:

/**
 * 表格层级合并工具函数
 * 返回合并单元格的下标数组
 * @param {Array} data - 要合并的表格数据
 * @param {Array} columns - 表格所有列名(顺序决定了表格中列的排列)
 * @param {Array} mergeColumns - 需要参与合并的列名数组
 * @returns {Array} - 合并单元格的下标数组,每个元素格式为 {row: 开始行, col: 列索引, rowspan: 行数}
 */
export function getMergedCells(data, columns, mergeColumns) {
  const mergedCells = [];
  // 使用队列来管理需要合并的区间,每个元素包含起始位置、结束位置和当前深度
  const mergeQueue = [{ start: 0, end: data.length, depth: 0 }];

  // 循环处理队列中的每个区间
  while (mergeQueue.length > 0) {
    const { start, end, depth } = mergeQueue.pop();
    // 如果深度超过需要合并的列数,则停止处理
    if (depth >= mergeColumns.length) continue;

    // 获取当前深度需要合并的列名
    const col = mergeColumns[depth];
    let pos = start;
    
    // 在当前区间内查找可以合并的连续相同值
    while (pos < end) {
      let i = pos;
      // 找到连续相同值的结束位置
      while (i < end - 1 && data[i][col] === data[i + 1][col]) i++;
      
      // 如果有连续相同值(长度大于1)
      if (i > pos) {
        // 获取列在表格中的索引位置
        const colIndex = columns.indexOf(col);
        // 记录合并信息
        mergedCells.push({ row: pos, col: colIndex, rowspan: i - pos + 1 });
        
        // 如果还有更深层级需要合并,则将当前合并区间加入队列
        if (depth < mergeColumns.length - 1) {
          mergeQueue.push({ start: pos, end: i + 1, depth: depth + 1 });
        }
      }
      // 移动到下一个不相同的值开始的位置
      pos = i + 1;
    }
  }
  return mergedCells;
}

算法二:基于递归的实现

第二种算法采用递归方式实现:

/**
 * 表格层级合并工具函数
 * 返回合并单元格的下标数组
 * @param {Array} data - 要合并的表格数据
 * @param {Array} columns - 表格所有列名(顺序决定了表格中列的排列)
 * @param {Array} mergeColumns - 需要参与合并的列名数组
 * @returns {Array} - 合并单元格的下标数组,每个元素格式为 {row: 开始行, col: 列索引, rowspan: 行数}
 */
export function getMergedCells(data, columns, mergeColumns) {
  // 参数校验,确保必要的参数存在且不为空
  if (
    !data ||
    !data.length ||
    !columns ||
    !columns.length ||
    !mergeColumns ||
    !mergeColumns.length
  ) {
    return [];
  }

  // 存储合并单元格信息的数组
  const mergedCells = [];

  /**
   * 递归处理函数,用于查找和记录需要合并的单元格
   * @param {Number} start - 当前处理区间的起始行索引
   * @param {Number} end - 当前处理区间的结束行索引(不包含)
   * @param {Number} depth - 当前处理的合并列深度(在mergeColumns中的索引)
   */
  const mergeRecursive = (start, end, depth) => {
    // 如果深度超过需要合并的列数,则停止递归
    if (depth >= mergeColumns.length) return;

    // 获取当前深度需要处理的列名
    const col = mergeColumns[depth];
    // 初始化当前位置为区间起始位置
    let pos = start;

    // 遍历当前区间的所有行
    while (pos < end) {
      let i = pos;
      // 查找连续相同值的范围
      // 如果当前行和下一行在当前列的值相同,则继续向后查找
      while (i < end - 1 && data[i][col] === data[i + 1][col]) {
        i++;
      }

      // 如果找到了连续相同的值(范围大于1行)
      if (i > pos) {
        // 获取列在表格中的索引位置
        const colIndex = columns.indexOf(col);
        // 记录合并信息:起始行、列索引、跨越行数
        mergedCells.push({
          row: pos,
          col: colIndex,
          rowspan: i - pos + 1,
        });

        // 如果还有更深层级需要处理,则递归处理子区间
        if (depth < mergeColumns.length - 1) {
          mergeRecursive(pos, i + 1, depth + 1);
        }
      }

      // 移动到下一组不相同的值开始的位置
      pos = i + 1;
    }
  };

  // 从第0行开始,到数据末尾,深度为0开始递归处理
  mergeRecursive(0, data.length, 0);
  return mergedCells;
}

测试输出

// 使用示例
const sampleData = [
    { category: '电子产品', region: '华东', year: 2022, quarter: 'Q1', sales: 120000 },
    { category: '电子产品', region: '华东', year: 2022, quarter: 'Q2', sales: 150000 },
    { category: '电子产品', region: '华东1', year: 2022, quarter: 'Q3', sales: 180000 },
    { category: '电子产品', region: '华东1', year: 2022, quarter: 'Q4', sales: 210000 },
    { category: '电子产品', region: '华南', year: 2022, quarter: 'Q1', sales: 90000 },
    { category: '电子产品', region: '华南', year: 2022, quarter: 'Q2', sales: 110000 },
    { category: '服装', region: '华东', year: 2022, quarter: 'Q1', sales: 80000 },
    { category: '服装', region: '华东', year: 2022, quarter: 'Q2', sales: 95000 },
    { category: '服装', region: '华南', year: 2022, quarter: 'Q1', sales: 70000 },
    { category: '服装', region: '华南', year: 2022, quarter: 'Q2', sales: 85000 },
    { category: '食品', region: '华东', year: 2023, quarter: 'Q1', sales: 50000 },
    { category: '食品', region: '华东', year: 2023, quarter: 'Q2', sales: 60000 }
];


// 指定合并列的顺序
const mergeColumns = ["category", "region", "year", "quarter"];

// 获取合并单元格的下标数组
const mergedCells = getMergedCells(sampleData, mergeColumns);

console.log("合并单元格下标数组:", mergedCells);

/**
合并单元格下标数组: [
  { row: 0, col: 0, rowspan: 6 },
  { row: 0, col: 1, rowspan: 2 },
  { row: 0, col: 2, rowspan: 2 },
  { row: 2, col: 1, rowspan: 2 },
  { row: 2, col: 2, rowspan: 2 },
  { row: 4, col: 1, rowspan: 2 },
  { row: 4, col: 2, rowspan: 2 },
  { row: 6, col: 0, rowspan: 4 },
  { row: 6, col: 1, rowspan: 2 },
  { row: 6, col: 2, rowspan: 2 },
  { row: 8, col: 1, rowspan: 2 },
  { row: 8, col: 2, rowspan: 2 },
  { row: 10, col: 0, rowspan: 2 },
  { row: 10, col: 1, rowspan: 2 },
  { row: 10, col: 2, rowspan: 2 }
]
**/

交互式测试工具

为了方便对比这两种算法的效果,我们开发了一个交互式测试工具,具有以下功能:

  1. 数据编辑器:可以自定义测试数据
  2. 列配置:可以指定表格的列顺序
  3. 合并列配置:可以指定需要进行层级合并的列
  4. 预设数据:提供多种典型数据场景进行测试
  5. 实时对比:同时展示两种算法的合并结果并进行比对