【问题标题】:Sort a range or array based on two columns that contain the date and time根据包含日期和时间的两列对范围或数组进行排序
【发布时间】:2018-04-16 15:14:02
【问题描述】:

目前,我正在尝试为 Google 表格创建一个 Google Apps 脚本,该脚本将允许为即将发生的事件批量添加每周重复发生的事件。然后,我的同事将对这些添加的事件进行细微更改(例如,更正日期和时间、更改联系人、添加事件所需的材料等)。

到目前为止,我已经编写了以下脚本:

function CopyWeeklyEventRows() {
  var ss = SpreadsheetApp.getActiveSheet();
  var repeatingWeeks = ss.getRange(5,1).getValue(); // gets how many weeks it should repeat
  var startDate = ss.getRange(6, 1).getValue(); // gets the start date
  var startWeekday = startDate.getDay(); // gives the weekday of the start date
  var regWeek = ss.getRange(9, 2, 4, 7).getValues(); // gets the regular week data
  var regWeekdays = new Array(regWeek.length); // creates an array to store the weekdays of the regWeek
  var ArrayStartDate = new Array(startDate); // helps to store the We

  for (var i = 0; i < regWeek.length; i++){ // calculates the difference between startWeekday and each regWeekdays
    regWeekdays[i] = regWeek[i][1].getDay() - startWeekday;
    Logger.log(regWeekdays[i]);
    // Add 7 to move to the next week and avoid negative values
    if (regWeekdays[i] < 0) {
      regWeekdays[i] = regWeekdays[i] + 7;
    }
    // Add days according to difference between startWeekday and each regWeekdays
    regWeek[i][0] = new Date(ArrayStartDate[0].getTime() + regWeekdays[i]*3600000*24);
  }
  // I'm struggling with this line. The array regWeek is not sorted:
  //regWeek.sort([{ column: 1, ascending: true }]);
  ss.getRange(ss.getLastRow() + 1, 2, 4, 7).setValues(regWeek); // copies weekly events after the last row
}

它允许根据开始日期将一周的重复事件添加到电子表格的概览部分。如果开始日期是星期二,则从星期二开始添加常规周。 However, the rows are not sorted according to the dates: .

在将行添加到概览之前,如何按升序日期(后跟时间)对行进行排序?

我对类似问题的搜索显示Google Script sort 2D Array by any column,这是我找到的最接近的命中。使用 sort 行运行我的脚本时会显示相同的错误消息。我不明白Rangearray 之间的区别,这可能有助于解决问题。

为了给你一个更广泛的了解,这是我目前正在做的事情:

  • 我注意到添加时格式不一定会保留 新的重复事件。到目前为止,我还没有找到规则并格式化为 进行第二步。
  • 目前的一个缺点是每周重复事件部分是 固定的。我试图找到最后填写的条目并使用它来设置 regWeek 的范围,但卡住了。
  • 使用 A 列从添加中排除重复事件 使用下拉菜单处理。
  • 允许我的同事使用 下拉菜单(例如 A26)。然后应该通过排序添加此事件 一周中的正确日期和开始时间。排序会进来 方便。

在此先感谢您提供有关排序的意见以及有关如何总体改进代码的建议。

A demo version of the spreadsheet

UpdateV01:

这里是复制和排序的代码行(首先按日期,然后按时间)

ss.getRange(ss.getLastRow()+1,2,4,7).setValues(regWeek); // copies weekly events after the last row
ss.getRange(ss.getLastRow()-3,2,4,7).sort([{column: 2, ascending: true}, {column: 4, ascending: true}]); // sorts only the copied weekly events chronologically

正如@tehhowch 指出的那样,这很慢。最好在写作之前进行排序。 我将实现此方法并在此处发布。

UpdateV02:

regWeek.sort(function (r1, r2) {
// sorts ascending on the third column, which is index 2
return r1[2] - r2[2];
});
regWeek.sort(function (r1, r2) {
// r1 and r2 are elements in the regWeek array, i.e.
// they are each a row array if regWeek is an array of arrays:
// Sort ascending on the first column, which is index 0:
// if r1[0] = 1, r2[0] = 2, then 1 - 2 is -1, so r1 sorts before r2
return r1[0] - r2[0];
});

UpdateV03:

这里尝试在几周内重复重复发生的事件。还不知道如何包含整个“周”的推送。

// Repeat week for "A5" times and add to start/end date
for (var j = 0; j < repeatingWeeks; j++){
  for (var i = 0; i < numFilledRows; i++){
  regWeekRepeated[i+j*6][0] = new Date(regWeek[i][0].getTime() + j*7*3600000*24); // <-This line leads to an error message
  regWeekRepeated[i+j*6][3] = new Date(regWeek[i][3].getTime() + j*7*3600000*24);
  }
}

我的问题得到了解答,我能够使代码按预期工作。

【问题讨论】:

  • 不清楚 - 您是要对要写入的位 (regWeek) 还是整个“即将发生的事件”位进行排序,即单元格 B22:K?两者的解决方案不同。您的排序对象适用于电子表格Range,但不是对 Javascript Array 进行排序的有效函数。
  • @tehhowch 感谢您指出歧义。我想对将要写入的位进行排序(regWeek)。对位进行排序后,for 循环将允许我在“即将发生的事件”部分添加 X(以 A5 输入)周(每个循环将日期增加 7 天)。根据您的评论,我会将当前for 循环的代码从Array 更改为Range。数组regWeekdaysArrayStartDate 被用作解决方法,因为我还不知道如何操作Range 中的数据。
  • 您不想对Range 进行批量操作 - 这非常慢,因为每个操作都必须使用 JavaScript 和电子表格之间的接口代码。您希望尽可能操作原生 JavaScript,并限制使用接口代码,如 getRangegetValue 查看 Mozilla 开发者网络上的数组排序

标签: sorting google-apps-script google-sheets


【解决方案1】:

鉴于您的评论-您想对书面块进行排序-您有两种方法可用。一种是使用电子表格服务的Range#sort(sortObject) 方法在写入后对写入的数据进行排序。另一种是在写入前对数据进行排序,使用JavaScript的Array#sort(sortFunction())方法。

目前,您的排序代码//regWeek.sort([{ column: 1, ascending: true }]); 正在尝试使用电子表格服务预期的排序对象对 JavaScript 数组进行排序。因此,您可以简单地将这个 .sort(...) 调用链接到您的 write 调用,因为 Range#setValues() 返回相同的 Range,允许重复调用 Range 方法(例如,设置值,然后应用格式等)。

这看起来像:

ss.getRange(ss.getLastRow() + 1, 2, regWeek.length, regWeek[0].length)
   .setValues(regWeek)
   /* other "chainable" Range methods you want to apply to
       the cells you just wrote to. */
   .sort([{column: 1, ascending: true}, ...]);

在这里,我更新了您访问以引用您尝试写入的数据的范围 - regWeek - 以便它始终是保存数据的正确大小。我还直观地分解了单行,以便您可以更好地看到电子表格服务调用之间发生的“链接”。

另一种方法 - 在写入之前排序 - 会更快,尤其是随着排序的大小和复杂性增加。对范围进行排序背后的想法是,您需要使用一个函数,当第一个索引的值应该在第二个索引之前时返回一个负值,当第一个索引的值应该在第二个索引之后返回一个正值,如果它们是零值相等的。这意味着返回boolean 的函数不会像人们想象的那样排序,因为false0 在Javascript 中是等价的,而true1 也是等价的。

您的排序如下所示,假设 regWeek 是一个数组数组,并且您正在对数值(或至少将转换为数字的值,如 Dates)进行排序。

regWeek.sort(function (r1, r2) {
  // r1 and r2 are elements in the regWeek array, i.e.
  // they are each a row array if regWeek is an array of arrays:
  // Sort ascending on the first column, which is index 0:
  // if r1[0] = 1, r2[0] = 2, then 1 - 2 is -1, so r1 sorts before r2
  return r1[0] - r2[0];
});

我强烈建议查看Array#sort documentation

【讨论】:

  • 感谢您解释不同的方法。关于第一种方法:我不知道为range 方法创建“链”,即使我在代码中使用了它。链总是从左到右处理。它是否正确?关于第二种方法:我已经成功地实现了您的代码 sn-p 首先按time 排序,然后按date。在我的搜索过程中,我遇到了这种方法,但很难理解这个概念。它需要更多的阅读/练习,但现在很有意义,我将使用它进行进一步的排序。
  • @AlexJackson 是的,JavaScript Object 的点运算符需要右侧的属性名称和左侧的对象引用。 JS 和其他一些语言会自动删除我用来格式化代码的那些换行符和“组织空白”,如果代码没有评估为正确的 JavaScript,则会发出SyntaxError
【解决方案2】:

您可以在设置 regWeek 变量之前对“每周事件”范围进行排序。然后在处理它之前,范围将按照您想要的顺序。或者您可以在设置数据后对整个“概览”范围进行排序。这是一个快速函数,您可以调用它来按多列对范围进行排序。您当然可以调整它以对“每周事件”范围而不是“概述”范围进行排序。

function sortRng() {
  var ss = SpreadsheetApp.getActiveSheet();
  var firstRow = 22; var firstCol = 1;
  var numRows = ss.getLastRow() - firstRow + 1; 
  var numCols = ss.getLastColumn();
  var overviewRng = ss.getRange(firstRow, firstCol, numRows, numCols);
  Logger.log(overviewRng.getA1Notation());
  overviewRng.sort([{column: 2, ascending: true}, {column: 4, ascending: true}]);
}

至于在每周事件部分中获取已填充的行数,您需要搜索一个始终有数据的列,如果任何行有数据(如开始日期列 b),遍历值和第一次它找到一个空白,返回那个数字。这将为您提供它需要复制的行数。警告:如果您在每周事件部分和概览部分之间的 B 列中没有至少一个空白值,您可能会得到不需要的结果。

function getNumFilledRows() {
  var ss = SpreadsheetApp.getActiveSheet();
  var eventFirstRow = 9; var numFilledRows = 0;
  var colToCheck = 'B';//the StartDate col which should always have data if the row is filled
  var vals = ss.getRange(colToCheck + eventFirstRow + ":" + colToCheck).getValues();
  for (i = 0; i < vals.length; i++) {
    if (vals[i][0] == '') {
      numFilledRows = i;
      break;
    }
  }
  Logger.log(numFilledRows);
  return numFilledRows;
}

编辑: 如果您只想在编写之前在javascript中对数组进行排序,并且您想先按开始日期排序,然后按一天中的时间排序,您可以制作一个临时数组,并为每一行添加一个日期和时间组合的列。 array.sort() 按字母顺序对日期进行排序,因此您需要将该日期转换为整数。然后您可以按新列对数组进行排序,然后从每一行中删除新列。我在下面包含了一个执行此操作的函数。它可能会更紧凑,但我认为它可能像这样更清晰。

function sortDates() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var vals = ss.getActiveSheet().getRange('B22:H34').getDisplayValues(); //get display values because getValues returns time as weird date 1899 and wrong time.
  var theDate = new Date(); var newArray = []; var theHour = ''; var theMinutes = '';
  var theTime = '';
  //Create a new array that inserts date and time as the first column in each row
  vals.forEach(function(aRow) {
    theTime = aRow[2];//hardcoded - assumes time is the third column that you grabbed
    //get the hours (before colon) as a number
    theHour = Number(theTime.substring(0,theTime.indexOf(':'))); 
    //get the minutes(after colon) as a number
    theMinutes = Number(theTime.substring(theTime.indexOf(':')+1)); 
    theDate = new Date(aRow[0]);//hardcoded - assumes date is the first column you grabbed.
    theDate.setHours(theHour);
    theDate.setMinutes(theMinutes);
    aRow.unshift(theDate.getTime()); //Add the date and time as integer to the first item in the aRow array for sorting purposes.
    newArray.push(aRow);
  });
  //Sort the newArray based on the first item of each row (date and time as number)
  newArray.sort((function(index){
    return function(a, b){
        return (a[index] === b[index] ? 0 : (a[index] < b[index] ? -1 : 1));
    };})(0));
  //Remove the first column of each row (date and time combined) that we added in the first step
  newArray.forEach(function(aRow) {
    aRow.shift();
  });
  Logger.log(newArray); 
} 

【讨论】:

  • @Hurtson 感谢您的反馈。两个函数 (sortRng,getNumFilledRows) 都运行良好。我已经对sortRng 进行了试验并对其进行了调整,以便它执行以下操作: - 将重复事件从行 9:12 复制到“即将发生的事件”部分并根据开始日期更改日期 - (仅)排序复制的事件我注意到我之前的代码从最后一行开始对行进行排序。这就是为什么看不到任何变化的原因。我会将更改后的代码添加到我的问题中。
  • 很好,很高兴我能帮上忙。我注意到上面有人提到排序范围很慢。但是在我的执行日志中,它说排序花费了 0 秒,该函数的总执行时间为 0.207 秒。 “最慢”的功能是获取工作表的最后一列,耗时 0.104 秒。获取最后一行需要 0.096 秒。因此,根据执行日志,范围的排序比那些基本命令要快得多。
  • @AlexJackson 我编辑了我的答案并添加了一个独立的排序函数,该函数在 javascript 中进行排序。您可以调整该函数以在您的脚本中工作。
  • 感谢提出独立排序功能。最近几天我已经完成了这个功能以获得更好的理解。总结该功能:您重新创建日期(包括小时+分钟)并将其放在一列中。此列放在开头,用于排序。排序本身将元素 a 与 b 进行比较。如果它们相同,则什么也不会发生,如果 a 小于 b,则它在前面移动,否则它在后面移动。我对你使用的部分 newArray.push(aRow) 很感兴趣。可以将它与 for 循环结合起来并重复它,例如5 周。
  • 是的,就是这样。 newArray.push(aRow) 只是在每次循环时将我们创建的行添加到 newArray 中。您可以只修改数组的每一行而不是创建一个全新的数组,但我认为这样更容易看到它。您当然可以将它与 for 循环一起使用。事实上,在代码中,它已经在一个 forEach 循环中了。我从这里的一个老问题中得到的排序功能:stackoverflow.com/questions/2824145/….
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-09-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-29
  • 2016-09-01
相关资源
最近更新 更多