Более быстрый способ найти первую пустую строку

Я создал script, который каждые несколько часов добавляет новую строку в электронную таблицу Google Apps.

Это функция, которую я создал, чтобы найти первую пустую строку:

function getFirstEmptyRow() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var cell = spr.getRange('a1');
  var ct = 0;
  while ( cell.offset(ct, 0).getValue() != "" ) {
    ct++;
  }
  return (ct);
}

Он отлично работает, но при достижении около 100 строк он становится очень медленным, даже десять секунд. Я волнуюсь, что, достигнув тысяч строк, это будет слишком медленно, возможно, в тайм-аут или хуже. Есть ли лучший способ?

Ответ 1

В блоге Google Apps Script появилась надпись оптимизация операций с электронными таблицами, в которой говорилось о пакетных чтениях и записи, которые могли бы реально ускорить процесс. Я пробовал свой код на электронной таблице со 100 строками, и это заняло около семи секунд. Используя Range.getValues(), пакетная версия занимает одну секунду.

function getFirstEmptyRow() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var column = spr.getRange('A:A');
  var values = column.getValues(); // get all data in one call
  var ct = 0;
  while ( values[ct][0] != "" ) {
    ct++;
  }
  return (ct);
}

Если электронная таблица становится достаточно большой, вам может потребоваться захватить данные в кусках из 100 или 1000 строк вместо захвата всего столбца.

Ответ 2

У этого вопроса теперь было больше 12K просмотров - так что время для обновления, так как характеристики производительности New Sheets отличаются от Serge ran его начальные тесты.

Хорошая новость: производительность намного лучше по всем направлениям!

Fastest:

Как и в первом тесте, чтение данных листа только один раз, а затем работа с массивом, принесло огромную выгоду. Интересно, что оригинальная функция Дон выполнялась намного лучше, чем модифицированная версия, которую Серг тестировал. (Похоже, что while быстрее, чем for, что не является логичным.)

Среднее время выполнения для данных образца составляет 38 мс, по сравнению с предыдущим 168 мс.

// Don array approach - checks first column only
// With added stopping condition & correct result.
// From answer https://stackoverflow.com/a/9102463/1677912
function getFirstEmptyRowByColumnArray() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var column = spr.getRange('A:A');
  var values = column.getValues(); // get all data in one call
  var ct = 0;
  while ( values[ct] && values[ct][0] != "" ) {
    ct++;
  }
  return (ct+1);
}

Результаты тестирования:

Вот результаты, суммированные более 50 итераций в электронной таблице со 100 строками x 3 столбца (заполненные функцией тестирования Серга).

Названия функций соответствуют коду в script ниже.

screenshot

"Первая пустая строка"

Первоначальный запрос состоял в том, чтобы найти первую пустую строку. Ни один из предыдущих сценариев на самом деле не доставляет этого. Многие проверяют только один столбец, что означает, что они могут давать ложные положительные результаты. Другие только находят первую строку, которая следует за всеми данными, что означает, что пустые строки в несмежных данных пропущены.

Здесь функция, соответствующая спецификации. Он был включен в тесты, и, хотя он был медленнее, чем простой одноколоночный контролер, он пришел к респектабельным 68 мс, 50% премию за правильный ответ!

/**
 * Mogsdad "whole row" checker.
 */
function getFirstEmptyRowWholeRow() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var range = sheet.getDataRange();
  var values = range.getValues();
  var row = 0;
  for (var row=0; row<values.length; row++) {
    if (!values[row].join("")) break;
  }
  return (row+1);
}

Завершить script:

Если вы хотите повторить тесты или добавить свою собственную комбинацию в качестве сравнения, просто возьмите целую script и используйте ее в электронной таблице.

/**
 * Set up a menu option for ease of use.
 */
function onOpen() {
  var menuEntries = [ {name: "Fill sheet", functionName: "fillSheet"},
                      {name: "test getFirstEmptyRow", functionName: "testTime"}
                     ];
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  sh.addMenu("run tests",menuEntries);
}

/**
 * Test an array of functions, timing execution of each over multiple iterations.
 * Produce stats from the collected data, and present in a "Results" sheet.
 */
function testTime() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  ss.getSheets()[0].activate();
  var iterations = parseInt(Browser.inputBox("Enter # of iterations, min 2:")) || 2;

  var functions = ["getFirstEmptyRowByOffset", "getFirstEmptyRowByColumnArray", "getFirstEmptyRowByCell","getFirstEmptyRowUsingArray", "getFirstEmptyRowWholeRow"]

  var results = [["Iteration"].concat(functions)];
  for (var i=1; i<=iterations; i++) {
    var row = [i];
    for (var fn=0; fn<functions.length; fn++) {
      var starttime = new Date().getTime();
      eval(functions[fn]+"()");
      var endtime = new Date().getTime();
      row.push(endtime-starttime);
    }
    results.push(row);
  }

  Browser.msgBox('Test complete - see Results sheet');
  var resultSheet = SpreadsheetApp.getActive().getSheetByName("Results");
  if (!resultSheet) {
    resultSheet = SpreadsheetApp.getActive().insertSheet("Results");
  }
  else {
    resultSheet.activate();
    resultSheet.clearContents();
  }
  resultSheet.getRange(1, 1, results.length, results[0].length).setValues(results);

  // Add statistical calculations
  var row = results.length+1;
  var rangeA1 = "B2:B"+results.length;
  resultSheet.getRange(row, 1, 3, 1).setValues([["Avg"],["Stddev"],["Trimmed\nMean"]]);
  var formulas = resultSheet.getRange(row, 2, 3, 1);
  formulas.setFormulas(
    [[ "=AVERAGE("+rangeA1+")" ],
     [ "=STDEV("+rangeA1+")" ],
     [ "=AVERAGEIFS("+rangeA1+","+rangeA1+',"<"&B$'+row+"+3*B$"+(row+1)+","+rangeA1+',">"&B$'+row+"-3*B$"+(row+1)+")" ]]);
  formulas.setNumberFormat("##########.");

  for (var col=3; col<=results[0].length;col++) {
    formulas.copyTo(resultSheet.getRange(row, col))
  }

  // Format for readability
  for (var col=1;col<=results[0].length;col++) {
    resultSheet.autoResizeColumn(col)
  }
}

// Omiod original function.  Checks first column only
// Modified to give correct result.
// question https://stackoverflow.com/questions/6882104
function getFirstEmptyRowByOffset() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var cell = spr.getRange('a1');
  var ct = 0;
  while ( cell.offset(ct, 0).getValue() != "" ) {
    ct++;
  }
  return (ct+1);
}

// Don array approach - checks first column only.
// With added stopping condition & correct result.
// From answer https://stackoverflow.com/a/9102463/1677912
function getFirstEmptyRowByColumnArray() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var column = spr.getRange('A:A');
  var values = column.getValues(); // get all data in one call
  var ct = 0;
  while ( values[ct] && values[ct][0] != "" ) {
    ct++;
  }
  return (ct+1);
}

// Serge getFirstEmptyRow, adapted from Omiod's, but
// using getCell instead of offset. Checks first column only.
// Modified to give correct result.
// From answer /questions/199257/faster-way-to-find-the-first-empty-row/1088394#1088394
function getFirstEmptyRowByCell() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var ran = spr.getRange('A:A');
  var arr = []; 
  for (var i=1; i<=ran.getLastRow(); i++){
    if(!ran.getCell(i,1).getValue()){
      break;
    }
  }
  return i;
}

// Serges adaptation of Don array answer.  Checks first column only.
// Modified to give correct result.
// From answer /questions/199257/faster-way-to-find-the-first-empty-row/1088394#1088394
function getFirstEmptyRowUsingArray() {
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sh.getActiveSheet();
  var data = ss.getDataRange().getValues();
  for(var n=0; n<data.length ;  n++){
    if(data[n][0]==''){n++;break}
  }
  return n+1;
}

/**
 * Mogsdad "whole row" checker.
 */
function getFirstEmptyRowWholeRow() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var range = sheet.getDataRange();
  var values = range.getValues();
  var row = 0;
  for (var row=0; row<values.length; row++) {
    if (!values[row].join("")) break;
  }
  return (row+1);
}

function fillSheet(){
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sh.getActiveSheet();
  for(var r=1;r<1000;++r){
    ss.appendRow(['filling values',r,'not important']);
  }
}

// Function to test the value returned by each contender.
// Use fillSheet() first, then blank out random rows and
// compare results in debugger.
function compareResults() {
  var a = getFirstEmptyRowByOffset(),
      b = getFirstEmptyRowByColumnArray(),
      c = getFirstEmptyRowByCell(),
      d = getFirstEmptyRowUsingArray(),
      e = getFirstEmptyRowWholeRow(),
      f = getFirstEmptyRowWholeRow2();
  debugger;
}

Ответ 4

Увидев это старое сообщение с 5k просмотров, я впервые проверил "лучший ответ" и был весьма удивлен его содержанием... это действительно был очень медленный процесс! тогда я почувствовал себя лучше, когда увидел, что Дон Киркби ответил, что массивный подход действительно намного эффективнее!

Но насколько эффективнее?

Итак, я написал этот небольшой тестовый код в электронной таблице с 1000 строками, и вот результаты: (неплохо!... не нужно указывать, какой из них какой...)

enter image description here enter image description here

и вот код, который я использовал:

function onOpen() {
  var menuEntries = [ {name: "test method 1", functionName: "getFirstEmptyRow"},
                      {name: "test method 2 (array)", functionName: "getFirstEmptyRowUsingArray"}
                     ];
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  sh.addMenu("run tests",menuEntries);
}

function getFirstEmptyRow() {
  var time = new Date().getTime();
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var ran = spr.getRange('A:A');
  for (var i= ran.getLastRow(); i>0; i--){
    if(ran.getCell(i,1).getValue()){
      break;
    }
  }
  Browser.msgBox('lastRow = '+Number(i+1)+'  duration = '+Number(new Date().getTime()-time)+' mS');
}

function getFirstEmptyRowUsingArray() {
  var time = new Date().getTime();
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sh.getActiveSheet();
  var data = ss.getDataRange().getValues();
  for(var n =data.length ; n<0 ;  n--){
    if(data[n][0]!=''){n++;break}
  }
  Browser.msgBox('lastRow = '+n+'  duration = '+Number(new Date().getTime()-time)+' mS');
}

function fillSheet(){
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sh.getActiveSheet();
  for(var r=1;r<1000;++r){
    ss.appendRow(['filling values',r,'not important']);
  }
}

И тестовую таблицу, чтобы попробовать сами :-)


ОБНОВЛЕНИЕ:

После комментария Mogsdad я должен упомянуть, что эти имена функций действительно плохой выбор... Это должно было быть что-то вроде getLastNonEmptyCellInColumnAWithPlentyOfSpaceBelow(), которое не очень элегантно (не так ли?), Но более точно и согласованно с тем, что оно фактически возвращает.

Комментарий:

Во всяком случае, моя цель была показать скорость выполнения обоих подходов, и он, очевидно, сделал это (не так ли? ;-)

Ответ 5

Я знаю, что это старый поток, и здесь были некоторые очень умные подходы.

Я использую script

var firstEmptyRow = SpreadsheetApp.getActiveSpreadsheet().getLastRow() + 1;

если мне нужна первая полностью пустая строка.

Если мне нужна первая пустая ячейка в столбце, я делаю следующее.

  • Моя первая строка обычно является строкой заголовка.
  • Моя вторая строка является скрытой строкой, и каждая ячейка имеет формулу

    =COUNTA(A3:A)
    

    Где A заменяется буквой столбца.

  • Мой script просто считывает это значение. Это довольно быстро обновляется по сравнению с script.

Один раз это не работает, и это когда я разрешаю пустым ячейкам разбивать столбец. Я еще не нуждался в исправлении, я подозреваю, что можно получить из COUNTIF или комбинированную функцию или одну из многих других встроенных.

РЕДАКТИРОВАТЬ: COUNTA справляется с пустым ячеек в пределах диапазона, поэтому беспокойство о "одном время это не работает" на самом деле не вызывает беспокойства. (Это может быть новое поведение с "новыми листами".)

Ответ 6

И почему бы не использовать appendRow?

var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
spreadsheet.appendRow(['this is in column A', 'column B']);

Ответ 7

Действительно, getValues ​​- хороший вариант, но вы можете использовать функцию .length для получения последней строки.

 function getFirstEmptyRow() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var array = spr.getDataRange().getValues();
  ct = array.length + 1
  return (ct);
}

Ответ 8

У меня подобная проблема. Прямо сейчас это таблица со многими сотнями строк, и я ожидаю, что она вырастет до многих тысяч. (Я не видел, будет ли электронная таблица Google обрабатывать десятки тысяч строк, но я обязательно вернусь.)

Вот что я делаю.

  • Шаг вперед по столбцу сотнями, остановитесь, когда я на пустой строке.
  • Шаг назад через столбец по десяткам, ища первую непустую строку.
  • Шаг за шагом по столбцу, ищем первую пустую строку.
  • Верните результат.

Это зависит, конечно, от непрерывного контента. Не может быть случайных пустых строк. Или, по крайней мере, если вы это сделаете, результаты будут субоптимальными. И вы можете настроить приращения, если считаете это важным. Они работают для меня, и я нахожу, что разница в длительности между этапами 50 и 100 составляет пренебрежимо мало.

function lastValueRow() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var r = ss.getRange('A1:A');
  // Step forwards by hundreds
  for (var i = 0; r.getCell(i,1).getValue() > 1; i += 100) { }
  // Step backwards by tens
  for ( ; r.getCell(i,1).getValue() > 1; i -= 10) { }
  // Step forwards by ones
  for ( ; r.getCell(i,1).getValue() == 0; i--) { }
  return i;
}

Это намного быстрее, чем проверка каждой ячейки сверху. И если у вас есть другие столбцы, которые расширяют ваш рабочий лист, это может быть быстрее, чем проверка каждой ячейки снизу тоже.

Ответ 9

Я сохраняю дополнительную таблицу "обслуживания" на своих электронных таблицах, где храню такие данные.

Чтобы получить следующую свободную строку диапазона, я просто исследую соответствующую ячейку. Я могу получить значение мгновенно, потому что работа по поиску значения происходит, когда данные изменяются.

Формула в ячейке обычно выглядит примерно так:

=QUERY(someSheet!A10:H5010, 
    "select min(A) where A > " & A9 & " and B is null and D is null and H < 1")

Значение в A9 может периодически устанавливаться в некоторую строку, которая почти "достаточна" до конца.

Предостережение: я никогда не проверял, является ли это жизнеспособным для огромных наборов данных.

Ответ 10

Наконец, я получил для него однострочное решение.

var sheet = SpreadsheetApp.getActiveSpreadsheet();
var lastEmptyOnColumnB = sheet.getRange("B1:B"+sheet.getLastRow()).getValues().join(",").replace(/,,/g, '').split(",").length;

Это отлично работает для меня.

Ответ 11

Я подкрепил код ghoti, поставляемый таким образом, чтобы он искал пустую ячейку. Сравнение значений не работало над столбцом с текстом (или я не мог понять, как) вместо этого я использовал isBlank(). Обратите внимание, что значение сбрасывается с помощью! (перед переменной r), когда вы смотрите вперед, так как хотите, чтобы я увеличивался до тех пор, пока не будет найден пробел. Подготовьте лист на десять, чтобы прекратить уменьшать i, когда вы найдете ячейку, которая не пуста (удалена). Затем отложите лист на один на первый пробел.

function findRow_() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  ss.setActiveSheet(ss.getSheetByName("DAT Tracking"));
  var r = ss.getRange('C:C');
  // Step forwards by hundreds
  for (var i = 2; !r.getCell(i,1).isBlank(); i += 100) { }
  // Step backwards by tens
  for ( ; r.getCell(i,1).isBlank(); i -= 10) { }
  // Step forwards by ones
  for ( ; !r.getCell(i,1).isBlank(); i++) { }
  return i;

Ответ 12

Только мои два цента, но я делаю это все время. Я просто записываю данные в верхнюю часть листа. Это дата отменена (последняя сверху), но я все еще могу заставить ее делать то, что я хочу. В приведенном ниже коде хранились данные, которые он сбрасывает с сайта риэлтора в течение последних трех лет.

var theSheet = SpreadsheetApp.openById(zSheetId).getSheetByName('Sheet1');
theSheet.insertRowBefore(1).getRange("A2:L2").setValues( [ zPriceData ] );

Этот кусок функции скребка вставляет строку выше # 2 и записывает там данные. Первая строка - это заголовок, поэтому я не касаюсь этого. Я не приурочил его, но единственный раз, когда у меня возникла проблема, это когда сайт меняется.

Ответ 13

Использование indexOf - один из способов добиться этого:

function firstEmptyRow() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sh = ss.getActiveSheet();
  var rangevalues = sh.getRange(1,1,sh.getLastRow(),1).getValues(); // Column A:A is taken
  var dat = rangevalues.reduce(function (a,b){ return a.concat(b)},[]); // 
 2D array is reduced to 1D//
  // Array.prototype.push.apply might be faster, but unable to get it to work//
  var fner = 1+dat.indexOf('');//Get indexOf First empty row
  return(fner);
  }