• R语言已解决
  • 用 DT 包绘制表格时,如何使相同内容的单元格合并?

如题,在gender列,有两个单元格都是M,怎样让这两个单元格合并?

library(DT)
df <- data.frame(
  name = c("Alice",  "Bob", "Charlie"),
  gender = c("F", "M", "M"),
  age = c(30, 31, 32),
  stringsAsFactors = FALSE
)

datatable(df) 

    yuanfan 问题简单,逻辑也简单,但代码写起来简直是白了少年头,因为太琐碎了。要是放在平时,打死我也不愿写这种代码,费时间还攒不了什么经验值。正好赶上这周休病假差不多已经复活、加上这两天难得有一丝丝活雷锋精神、以及过两天就是学习雷锋纪念日,我就当积德行善、拼尽耐心写一段笨拙的 JS 代码吧:

    function mergeRows(table) {
      const tbody = table.querySelector('tbody');
      if (!tbody) return;
      // 先验证每一行的单元格数量是否相同;若不同,就不尝试合并了(可能已经合并过)
      const rows = tbody.querySelectorAll('tr'), nrow = rows.length;
      if (nrow < 2) return;
      
      let ncol = rows[0].querySelectorAll(':scope > td').length;
      for (let i = 1; i < nrow; i++) {
        if (rows[i].querySelectorAll(':scope > td').length !== ncol) return;
      }
    
      let delCells = [];  // 待移除的重复单元格
      // 寻找每一列上下相同的相邻单元格,并用 rowspan 属性合并之
      for (let j = 0; j < ncol; j++) {
        // 第 j 列里的所有单元格
        let cells = tbody.querySelectorAll('tr > td:nth-child(' + (j + 1) + ')');
        let k = 0;  // 计数,看有几个相邻单元格相同
        for (let i = 1; i < nrow; i++) {
          // 向顶层单元格添加 rowspan 属性
          function addRowSpan(i) {
            if (k === 0) return;
            rows[i].querySelector(':scope > td:nth-child(' + (j + 1) + ')')
                   .setAttribute('rowspan', k + 1);
          }
          if (cells[i].innerHTML === cells[i - 1].innerHTML) {
            k++;  // 两行相同的话,计数器 k 加一
            delCells.push(cells[i]);  // 事后删除当前单元格
            // 若到达最后一行,至此合并
            if (i == rows.length - 1) addRowSpan(i - k);
          } else {
            // 若相邻两行不同,那么着手合并之前找到的相同单元格
            addRowSpan(i - 1 - k);
            k = 0;  // 重置计数器
          }
        }
      }
      delCells.forEach(cell => cell.remove());
    }

    找到页面上所有表格,合并相同单元格:

    document.querySelectorAll('table').forEach(table => mergeRows(table));

    好累,我得喘会儿。

    不知道 ChatGPT 会不会几秒钟就写出来了。我还没玩过这东西。

      yihui
      ChatGPT 交的作业,我完全不懂 JS,麻烦雷锋检查一下。

      // Get the HTML table element
      var table = document.getElementById("myTable");
      
      // Loop through each row in the table (excluding the first row)
      for (var i = 1; i < table.rows.length; i++) {
        var currCell = table.rows[i].cells[1];
        var prevCell = table.rows[i-1].cells[1];
        
        // If the content of the current cell matches the content of the previous cell in the gender column,
        // merge the current cell with the previous cell and update the rowspan of the previous cell
        if (currCell.innerHTML === prevCell.innerHTML && currCell.innerHTML === "M") {
          prevCell.rowSpan += 1;
          currCell.style.display = "none";
        }
      }

        Liechi 这个作业要是满分 100 分的话,我个人给它打 10 分吧;如果只是看楼主那个具体例子的问题的话,也可以打 80 分了。它没能理解的是楼主的例子只是一个简单示例,实际情况不会那么简单,所以写的代码需要有推广性。不过呢,它确实教给了我一些我不知道的新知识,就是表格有 .rows 属性可以直接得到行、行有 .cells 属性可以直接得到单元格、而单元格有 .rowSpan 属性可以直接操纵。这三点知识可以简化我的代码。等我明天睡醒了再来改改。

          yihui
          我是先喂给它这个例子,然后让它出代码实现上述目标。说实话,在这个例子里需要考虑些什么问题,我也不知道;我估计如果能给它更具体的指示,它应该可以做的更好。像这个例子,你说逻辑简单,但写代码麻烦,不知道这种时候chatGPT能不能帮助提高工作效率?

          我也不懂 JS,不过之前写的简单问答app就是在 ChatGPT 提示下写出来的。

          用 ChatGPT 写代码的缺点是,第一次给出的答案往往是一个不太精确但从 likelihood 角度最符合问题的答案。需要反复追问,直到给出相对符合设想的答案,这需要人类对具体的需求和编程语言的边界非常清楚,并且可以用自然语言精确描述出来(其实是一个不低的要求)。而 ChatGPT 的好处是可以提供一个大致可靠的框架和方向,比 Google 和 StackOverflow 提供的硬搜索结果要好得多,因为这里提供的「搜索」结果是根据现有知识合成的,针对性更强。

          我目前的观察是,需要强逻辑的问题(比如涉及某种数学),或者需要创造性解决方案的问题(比如利用洞察力找近路),答案基本不可靠。另外就是生成的 JS 代码质量比 R 代码的质量更高一些,不知是否和训练数据量的大小以及训练所用代码的整体水平有关:GPT-3 以上应该都是使用了 GitHub 上的所有公开代码库训练的。

          yihui 从 39 行代码精简到 37 行,删了一坨 querySelectorAll,大快人心。放狗一搜还学会了一个新技能 [...集合] 创建数组(尽管在此例中毫无必要)。

          function mergeRows(table) {
            if (!table.tBodies) return;
            const tbody = table.tBodies[0];
            // 先验证每一行的单元格数量是否相同;若不同,就不尝试合并了(可能已经合并过)
            const rows = [...tbody.rows], nrow = rows.length;
            if (nrow < 2) return;
            
            let ncol = rows[0].cells.length;
            for (let i = 1; i < nrow; i++) {
              if (rows[i].cells.length !== ncol) return;
            }
          
            let delCells = [];  // 待移除的重复单元格
            // 寻找每一列上下相同的相邻单元格,并用 rowspan 属性合并之
            for (let j = 0; j < ncol; j++) {
              // 第 j 列里的所有单元格
              let cells = tbody.querySelectorAll('tr > td:nth-child(' + (j + 1) + ')');
              let k = 0;  // 计数,看有几个相邻单元格相同
              for (let i = 1; i < nrow; i++) {
                // 向顶层单元格添加 rowspan 属性
                function addRowSpan(i) {
                  if (k > 0) rows[i].cells[j].rowSpan = k + 1;
                }
                if (cells[i].innerHTML === cells[i - 1].innerHTML) {
                  k++;  // 两行相同的话,计数器 k 加一
                  delCells.push(cells[i]);  // 事后删除当前单元格
                  // 若到达最后一行,至此合并
                  if (i == rows.length - 1) addRowSpan(i - k);
                } else {
                  // 若相邻两行不同,那么着手合并之前找到的相同单元格
                  addRowSpan(i - 1 - k);
                  k = 0;  // 重置计数器
                }
              }
            }
            delCells.forEach(cell => cell.remove());
          }
          
          document.querySelectorAll('table').forEach(table => mergeRows(table));

          这个 ChatGPT 确实是学习的好向导。

            yihui
            大神,你写的这段函数应该咋用呢?一整段给丢到htmlwidgets::JS()里面去?试了下,好使。

            datatable(
              df,
              callback = htmlwidgets::JS("")
            )

            yihui
            话又说回来,其实本帖的问题在发帖之前我就问了新必应好几遍。它其中一个答案就是 DT 包里面有一个 mergeRows 函数可以直接用。我说没有,它还继续死鸭子嘴硬非说让我升级DT包,说里面原来有 mergeRows,但是已经移除了,现在有个formatmergeRows。

            大神可以考虑下真得弄个 formatmergeRows,专门用来合并单元格。不过不知道这段 JS 代码效率如何,数据量大了以后会不会遇到速度与功能不可兼得的问题。

              yuanfan 这新必应还真是会一本正经地胡说八道。DT 的亲爹都不知道 DT 里有个 mergeRows() 函数或是什么 formatmergeRows()。

              如果只是合并一个完整的静态表格的单元格,那上面的代码已经完美解决问题,我当然可以打包它到 DT 里;问题是 DT 会分页展示数据,要是上一页的最后一行和下一页的第一行是需要合并的,这个情况是代码无法预知的,因为下一页的表格元素在页面上根本就不存在。如果这种情况可以不考虑,那也是可以打包的。

              20 天 后