【问题标题】:Script to permute columns, rows or any ranges用于置换列、行或任何范围的脚本
【发布时间】:2025-12-01 21:40:02
【问题描述】:

编辑:我更改了代码以包含按名称提供范围的可能性(以 A1 表示法),因为这可能比提供 Range 对象更有效(如果范围最终没有移动) 并且在简单的情况下肯定更容易使用。 AdamL 的想法(请参阅下面的答案)。


在某些电子表格中,我需要置换行或列。要求用户手动执行此操作不是很好。因此,在菜单中创建可以运行脚本的正确命令似乎是一个合理的解决方案。

奇怪的是,我找不到任何可以置换行/列的函数(无论是内置的还是由其他人编写的)。所以我自己写了一个,然后考虑出版它。但由于我对 JavaScript 和 Google Apps Script 的经验很少,我想让其他人检查这个功能。我也有一些问题。所以我们开始吧。


// Parameters:
// - ranges An Array with ranges which contents are to be permuted.
//          All the ranges must have the same size. They do not have to be
//          vectors (rows or columns) and can be of any size. They may come from
//          different sheets.
//          Every element of the array must be either a Range object or a string
//          naming the range in A1 notation (with or without sheet name).
// - permutation An Array with 0-based indexes determining desired permutation
//               of the ranges. i-th element of this array says to which range
//               should the contents of i-th range be moved.
// - temp A range of the same size as the ranges in "ranges". It is used to
//        temporarily store some ranges while permuting them. Thus the initial
//        contents of this range will be overwritten and its contents on exit is
//        unspecified. Yet if there is nothing to be moved ("ranges" has less
//        than 2 elements or all ranges are already on their proper places) this
//        range will not be used at all.
//        It is advised to make this range hidden so the "garbage" doesn't
//        bother user.
//        This can be either a Range object or a string naming the range in A1
//        notation (with or without sheet name) - just as with the "ranges".
// - sheet An optional Sheet object used to resolve range names without sheet
//         name. If none is provided active sheet is used. Note however that it
//         may cause issues if user changes the active sheet while the script is
//         running. Thus if you specify ranges by name without sheet names you
//         should provide this argument.
//
// Return Value:
// None.
//
// This function aims at minimizing moves of the ranges. It does at most n+m
// moves where n is the number of permuted ranges while m is the number of
// cycles within the permutation. For n > 0 m is at least 1 and at most n. Yet
// trivial 1-element cycles are handled without any moving (as there is nothing
// to be moved) so m is at most floor(n/2).
//
// For example to shift columns A, B and C by 1 in a cycle (with a temp in
// column D) do following:
//
// permuteRanges(
//   ["A1:A", "B1:B", "C1:C"],
//   [1, 2, 0],
//   "D1:D",
//   SpreadsheetApp.getActiveSheet()
// );
function permuteRanges(ranges, permutation, temp, sheet) {
  // indexes[i] says which range (index of ranges element) should be moved to
  // i-th position.
  var indexes = new Array(permutation.length);
  for(var i = 0; i < permutation.length; ++i)
    indexes[permutation[i]] = i;

  // Generating the above array is linear in time and requires creation of a
  // separate array.

  // Yet this allows us to save on moving ranges by moving most of them to their
  // final location with only one operation. (We need only one additional move
  // to a temporary location per each non-trivial cycle.)


  // Range extraction infrastructure.

  // This is used to store reference sheet once it will be needed (if it will be
  // needed). The reference sheet is used to resolve ranges provided by string
  // rather than by Range object.
  var realSheet;
  // This is used to store Range objects extracted from "ranges" on
  // corresponding indexes. It is also used to store Range object corresponding
  // to "temp" (on string index named "temp").
  var realRanges;

  // Auxiliary function which for given index obtains a Range object
  // corresponding to ranges[index] (or to temp if index is "temp").
  // This allows us to be more flexible with what can be provided as a range. So
  // we accept both direct Range objects and strings which are interpreted as
  // range names in A1 notation (for the Sheet.getRange function).
  function getRealRange(index) {
    // If realRanges wasn't yet created (this must be the first call to this
    // function then) create it.
    if(!realRanges) {
      realRanges = new Array(ranges.length);
    }

    // If we haven't yet obtained the Range do it now.
    if(!realRanges[index]) {
      var range;

      // Obtain provided range depending on whether index is "temp" or an index.
      var providedRange;
      if(index === "temp") {
        providedRange = temp;
      } else {
        providedRange = ranges[index];
      }

      // If corresponding "ranges" element is a string we have to obtain the
      // range from a Sheet...
      if(typeof providedRange === "string") {
        // ...so we have to first get the Sheet itself...
        if(!realSheet) {
          // ...if none was provided by the caller get currently active one. Yet
          // note that we do this only once.
          if(!sheet) {
            realSheet = SpreadsheetApp.getActiveSheet();
          } else {
            realSheet = sheet;
          }
        }
        range = realSheet.getRange(providedRange);
      } else {
        // But if the corresponding "ranges" element is not a string then assume
        // it is a Range object and use it directly.
        range = providedRange;
      }

      // Store the Range for future use. Each range is used twice (first as a
      // source and then as a target) except the temp range which is used twice
      // per cycle.
      realRanges[index] = range;
    }

    // We already have the expected Range so just return it.
    return realRanges[index];
  }


  // Now finally move the ranges.

  for(var i = 0; i < ranges.length; ++i) {
    // If the range is already on its place (because it was from the start or we
    // already moved it in some previous cycle) then don't do anything.
    // Checking this should save us a lot trouble since after all we are moving
    // ranges in a spreadsheet, not just swapping integers.
    if(indexes[i] == i) {
      continue;
    }

    // Now we will deal with (non-trivial) cycle of which the first element is
    // i-th. We will move the i-th range to temp. Then we will move the range
    // which must go on the (now empty) i-th position. And iterate the process
    // until we reach end of the cycle by getting to position on which the i-th
    // range (now in temp) should be moved.
    // Each time we move a range we mark it in indexes (by writing n on n-th
    // index) so that if the outer for loop reaches that index it will not do
    // anything more with it.

    getRealRange(i).moveTo(getRealRange("temp"));

    var j = i;
    while(indexes[j] != i) {
      getRealRange(indexes[j]).moveTo(getRealRange(j));

      // Swap index[j] and j itself.
      var old = indexes[j];
      indexes[j] = j;
      j = old;
    }

    getRealRange("temp").moveTo(getRealRange(j));
    // No need to swap since j will not be used anymore. Just write to indexes.
    indexes[j] = j;
  }
}

问题是:

  1. 这是否正确实施?可以改进吗?

  2. 参数验证怎么样?我应该这样做吗?无效怎么办?

  3. 我不确定是使用copyTo 还是moveTo。我决定使用moveTo,因为在我看来,这更像是我打算做的事情。但现在转念一想,我认为copyTo 可能会更有效。

  4. 我还注意到,移动的Range 并不总是被清除。尤其是在调试器中。

  5. 撤消/重做似乎是此功能的问题。似乎每个moveTo 都是电子表格上的一个单独操作(甚至更糟,但也许这只是我测试时谷歌文档的低响应性),并且撤消排列不是一个单一的操作。有什么办法吗?

  6. 我为该函数编写的文档声称它适用于不同的工作表甚至不同的电子表格。我实际上还没有检查过;)但 Google Apps 脚本文档似乎并没有否认它。会这样吗?


我不确定这是否是提出此类问题的合适场所(因为这不是一个真正的问题),但由于Google Apps Script community support is moving to Stack Overflow 我不知道还能去哪里问。

【问题讨论】:

    标签: google-apps-script


    【解决方案1】:

    您不认为在执行速度方面使用数组可能更有效吗?

    例如试试这个:(我在各处添加了日志以显示发生了什么) (另请注意,工作表限制为 255 列...注意列表长度)

    function permutation() {
    var sh = SpreadsheetApp.getActiveSheet();
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var lr = ss.getLastRow()
    var lc=ss.getLastColumn();
    var data = sh.getRange(1,1,lr,lc).getValues()
    Logger.log(data)
    
    var temp2= new Array();
    var h=data.length
    Logger.log(h)
    var w=data[0].length
    Logger.log(w)
    for(nn=0;nn<w;++nn){
    var temp1= new Array();
    for (tt=0;tt<h;++tt){
      temp1.push(data[tt][nn])
      }
      temp2.push(temp1)
      }
    Logger.log(temp2) 
    Logger.log(temp2.length) 
    Logger.log(temp2[0].length) 
    sh.getRange(1,1,lr,lc).clear()
    sh.getRange(1,1,lc,lr).setValues(temp2)
    }
    

    最好的问候, 塞尔吉

    【讨论】:

    • 当我测试各种方法时,getValues/setValues 在涉及带有公式或格式的单元格时似乎并不可靠。我也不知何故相信(但我没有测试它)这会更慢,因为它需要两次数据传输。我将其视为读取文件的内容以将其写回不同名称的文件(getValues/setValues),而不是仅重命名磁盘上的文件(moveTo/copyTo)。但我可能弄错了。
    【解决方案2】:

    Adam,根据我对 Apps Script GPF 的有限经验,我了解到最好尽可能限制 get 和 set 调用(您也可以在其中包括 moveTo/copyTo)。

    您是否认为将范围名称而不是范围作为参数传递会更好(为此,您可能还需要一种机制来传递工作表名称和电子表格键,以支持您的工作要求跨不同的工作表/电子表格),然后可以避免琐碎的“getRange”以及琐碎的“moveTo”。

    另外,如果您只是传输值,最好不要将其移动到临时范围,而是将这些数组分配给脚本中的一个变量,然后稍后可以在正确的位置“设置”该变量。但如果您需要复制格式或公式,那就另当别论了。

    【讨论】:

    • (1) 我认为接受范围名称而不是 Range 对象没有任何好处。它只会使功能更长。 (2) 我认为我尽可能少地做moveTo 来实现给定的排列。还是我弄错了? (3) 我不仅要传输值,还要传输公式和格式。 moveTo/copyTo 在我看来是这里唯一合理的选择。 (4) 而且我认为moveTo/copyTo 可能比 get/set 更有效,因为它不必向我或从我传输任何数据。还是我弄错了?
    • 关于 moveTo/copyTo 比 get/set 更有效的说法很可能是正确的,如果您需要复制格式和公式,那么无论如何这都是一个有争议的问题。但是我仍然不确定是否要在函数之外为不需要移动的范围执行 getRange。在最坏的情况下不会有净差;如果有许多不需要移动的范围,充其量只会有显着的改进。但是您的用例可能是后一种情况可能永远不会出现。
    • 好的,现在我明白了你想要实现的目标,即不传递Range 对象,而只传递它的名称。事实上,这可能证明是有用的。可悲的是,对其进行效率测试相当困难(或者至少我不知道该怎么做)。而且我还想象Range 对象被实现为轻包装器。所以我预计它的创建应该很便宜,除非你真的用那个 Range 对象做一些事情,否则应该没有显着的性能差异。但这只是一个疯狂的猜测。
    • 也许也可以扩展该函数以同时接受Range 和范围名称。如果要移动范围,它将调查相应 ranges 元素的类型并直接使用它(如果是 Range)或查询它(如果是名称) - 只有如果我们允许范围,这会更复杂来自不同的电子表格(但这是一个不太可能的用例)。这也将使使用该功能更加容易。好主意。我会调查的。
    • 我已经包含了按名称提供范围的可能性(在 A1 表示法中)。代码已经更改。感谢您的想法!